Managing comments across multiple social platforms has become one of the most demanding challenges for brands and developers alike. Effective social media comment moderation requires handling vastly different APIs, data formats, and rate limits while maintaining consistent response times for your community. This guide walks you through building a production-ready system that aggregates, normalizes, and processes comments from Facebook, Instagram, Twitter, YouTube, LinkedIn, Reddit, Bluesky, and Threads.
By the end of this article, you'll have working TypeScript code for a unified comment management system that scales to millions of interactions per day.
The Challenge of Multi-Platform Comments
Every social platform approaches comments differently. Facebook nests replies under parent comments with pagination tokens. Instagram ties comments to media objects with strict rate limits. YouTube uses a completely separate API from Google's other services. Twitter (X) treats replies as regular posts in a conversation thread.
These differences create three core problems:
Data inconsistency: Each platform returns different fields, formats timestamps differently, and structures user data in unique ways. A "comment" on YouTube looks nothing like a "reply" on Twitter in terms of API response.
Rate limit complexity: Facebook allows 200 calls per hour per user token. Twitter's API v2 has tiered access with different limits. YouTube enforces quota units rather than simple request counts. Managing these limits across platforms requires sophisticated throttling.
Real-time expectations: Users expect near-instant responses. When someone comments on your Instagram post, they don't care that you're also monitoring seven other platforms. Your system needs to aggregate comments quickly without hitting rate limits.
Here's the reality of what you're dealing with:
| Platform | API Style | Rate Limits | Comment Structure |
|---|---|---|---|
| Graph API | 200 calls/hour/user | Nested with pagination | |
| Graph API | 200 calls/hour/user | Flat, media-attached | |
| Twitter/X | REST + Streaming | Tier-based (10K-1M/month) | Conversation threads |
| YouTube | Data API v3 | 10,000 quota units/day | Threaded with replies |
| REST API | 100 calls/day (varies) | Organization-only | |
| OAuth REST | 60 requests/minute | Deeply nested trees | |
| Bluesky | AT Protocol | 3,000 points/5 min | Flat with reply refs |
| Threads | Graph API | Shared with Instagram | Similar to Instagram |
Platform Comparison: Features and Limitations
Before writing any code, you need to understand what each platform actually supports. Not every platform exposes comment data through their API, and some have significant restrictions.
type InboxFeature = 'messages' | 'comments' | 'reviews';
const INBOX_PLATFORMS = {
messages: ['facebook', 'instagram', 'twitter', 'bluesky', 'reddit', 'telegram'] as const,
// TikTok and Pinterest excluded: their APIs don't support reading comments
comments: ['facebook', 'instagram', 'twitter', 'bluesky', 'threads', 'youtube', 'linkedin', 'reddit'] as const,
reviews: ['facebook', 'googlebusiness'] as const,
} as const;
type CommentsPlatform = (typeof INBOX_PLATFORMS.comments)[number];
function isPlatformSupported(platform: string, feature: InboxFeature): boolean {
return (INBOX_PLATFORMS[feature] as readonly string[]).includes(platform);
}
function validatePlatformSupport(
platform: string,
feature: InboxFeature
): { valid: true } | { valid: false; error: string; supportedPlatforms: readonly string[] } {
if (!isPlatformSupported(platform, feature)) {
const featureLabel = feature === 'messages' ? 'direct messages' : feature;
return {
valid: false,
error: `Platform '${platform}' does not support ${featureLabel}`,
supportedPlatforms: INBOX_PLATFORMS[feature],
};
}
return { valid: true };
}
Note: TikTok and Pinterest are notably absent from comment support. Their APIs don't expose comment data for reading, only for posting in limited scenarios. Plan your multi-platform strategy accordingly.
LinkedIn presents a special case. The platform only allows comment access for organization (company) pages, not personal profiles. Your system needs to validate this:
function isLinkedInOrgAccount(
metadata: Map<string, unknown> | Record<string, unknown> | null | undefined
): boolean {
if (!metadata) return false;
// Handle Mongoose Map (common in MongoDB schemas)
if (metadata instanceof Map) {
return metadata.get('accountType') === 'organization' || metadata.has('selectedOrganization');
}
// Handle plain object
return metadata.accountType === 'organization' || 'selectedOrganization' in metadata;
}
function filterAccountsForFeature(
accounts: SocialAccount[],
feature: InboxFeature
): SocialAccount[] {
const supportedPlatforms = INBOX_PLATFORMS[feature] as readonly string[];
return accounts.filter((account) => {
if (!supportedPlatforms.includes(account.platform)) {
return false;
}
// LinkedIn requires organization account type for inbox features
if (account.platform === 'linkedin' && !isLinkedInOrgAccount(account.metadata)) {
return false;
}
return true;
});
}
Unified Comment Data Model
The foundation of effective multi-platform comment management is a normalized data model. Every platform's comment structure needs to map to a single interface that your application logic can work with consistently.
interface UnifiedComment {
// Identification
id: string; // Unique within your system
platformId: string; // Original ID from platform
platform: CommentsPlatform;
accountId: string; // Which connected account
// Content
text: string;
textHtml?: string; // Rich text if available
attachments: CommentAttachment[];
// Author
author: {
id: string;
username: string;
displayName: string;
avatarUrl?: string;
profileUrl?: string;
isVerified: boolean;
};
// Context
postId: string; // Parent post/video/media
parentCommentId?: string; // For nested replies
threadId?: string; // Conversation thread
// Metadata
createdAt: Date;
updatedAt?: Date;
likeCount: number;
replyCount: number;
// Moderation
status: 'pending' | 'approved' | 'hidden' | 'deleted';
sentiment?: 'positive' | 'neutral' | 'negative';
sentimentScore?: number; // -1 to 1
flags: string[]; // spam, offensive, etc.
// Platform-specific data (escape hatch)
rawData?: Record<string, unknown>;
}
interface CommentAttachment {
type: 'image' | 'video' | 'link' | 'sticker' | 'gif';
url: string;
thumbnailUrl?: string;
width?: number;
height?: number;
}
This model captures the essential fields while preserving platform-specific data in rawData for edge cases. The status and sentiment fields support moderation workflows that we'll build later.
Aggregating Comments Across Platforms
With your data model defined, you need infrastructure to fetch comments from multiple accounts simultaneously. The key challenges are handling failures gracefully (one platform being down shouldn't break everything) and managing timeouts.
interface AggregationError {
accountId: string;
accountUsername?: string;
platform: string;
error: string;
code?: string;
retryAfter?: number;
}
interface AggregatedResult<T> {
items: T[];
errors: AggregationError[];
}
async function aggregateFromAccounts<T>(
accounts: SocialAccount[],
fetcher: (account: SocialAccount) => Promise<T[]>,
options?: { timeout?: number }
): Promise<AggregatedResult<T>> {
const timeout = options?.timeout || 10000; // 10 second default
const results: T[] = [];
const errors: AggregationError[] = [];
const fetchPromises = accounts.map(async (account) => {
try {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeout);
});
const items = await Promise.race([fetcher(account), timeoutPromise]);
return { account, items, error: null };
} catch (error: unknown) {
const err = error as Error & { code?: string; retryAfter?: number };
return {
account,
items: [] as T[],
error: {
accountId: account._id?.toString() || account.id,
accountUsername: account.username,
platform: account.platform,
error: err.message || 'Unknown error',
code: err.code,
retryAfter: err.retryAfter,
},
};
}
});
const settledResults = await Promise.all(fetchPromises);
for (const result of settledResults) {
if (result.error) {
errors.push(result.error);
} else {
results.push(...result.items);
}
}
return { items: results, errors };
}
This pattern allows partial success. If your Facebook token expires but Twitter is working fine, you still get Twitter comments while logging the Facebook error for retry.
Comment Normalization Strategies
Each platform returns comments in different formats. You need transformer functions that convert platform-specific responses into your unified model. Here's how to structure these normalizers:
type CommentNormalizer = (
rawComment: unknown,
account: SocialAccount,
postId: string
) => UnifiedComment;
const normalizers: Record<CommentsPlatform, CommentNormalizer> = {
facebook: normalizeFacebookComment,
instagram: normalizeInstagramComment,
twitter: normalizeTwitterComment,
youtube: normalizeYouTubeComment,
linkedin: normalizeLinkedInComment,
reddit: normalizeRedditComment,
bluesky: normalizeBlueskyComment,
threads: normalizeThreadsComment,
};
function normalizeFacebookComment(
raw: FacebookCommentResponse,
account: SocialAccount,
postId: string
): UnifiedComment {
return {
id: `fb_${raw.id}`,
platformId: raw.id,
platform: 'facebook',
accountId: account._id.toString(),
text: raw.message || '',
attachments: raw.attachment ? [{
type: mapFacebookAttachmentType(raw.attachment.type),
url: raw.attachment.url,
thumbnailUrl: raw.attachment.media?.image?.src,
}] : [],
author: {
id: raw.from.id,
username: raw.from.id, // Facebook doesn't expose username
displayName: raw.from.name,
avatarUrl: `https://graph.facebook.com/${raw.from.id}/picture`,
profileUrl: `https://facebook.com/${raw.from.id}`,
isVerified: false, // Not available in basic response
},
postId,
parentCommentId: raw.parent?.id ? `fb_${raw.parent.id}` : undefined,
createdAt: new Date(raw.created_time),
likeCount: raw.like_count || 0,
replyCount: raw.comment_count || 0,
status: raw.is_hidden ? 'hidden' : 'approved',
flags: [],
rawData: raw,
};
}
function normalizeTwitterComment(
raw: TwitterTweetResponse,
account: SocialAccount,
postId: string
): UnifiedComment {
const author = raw.includes?.users?.find(u => u.id === raw.data.author_id);
return {
id: `tw_${raw.data.id}`,
platformId: raw.data.id,
platform: 'twitter',
accountId: account._id.toString(),
text: raw.data.text,
attachments: extractTwitterAttachments(raw.data, raw.includes),
author: {
id: raw.data.author_id,
username: author?.username || 'unknown',
displayName: author?.name || 'Unknown',
avatarUrl: author?.profile_image_url,
profileUrl: author ? `https://twitter.com/${author.username}` : undefined,
isVerified: author?.verified || false,
},
postId,
parentCommentId: raw.data.referenced_tweets?.find(
t => t.type === 'replied_to'
)?.id,
threadId: raw.data.conversation_id,
createdAt: new Date(raw.data.created_at),
likeCount: raw.data.public_metrics?.like_count || 0,
replyCount: raw.data.public_metrics?.reply_count || 0,
status: 'approved',
flags: [],
rawData: raw,
};
}
The normalizer pattern keeps platform-specific logic isolated. When Twitter changes their API (which happens frequently), you only update one function.
Building Moderation Workflows
Raw comment aggregation is just the beginning. Effective social media comment moderation requires workflows for reviewing, responding to, and actioning comments. Here's a state machine approach:
type ModerationAction =
| 'approve'
| 'hide'
| 'delete'
| 'reply'
| 'flag'
| 'escalate'
| 'assign';
interface ModerationRule {
id: string;
name: string;
conditions: RuleCondition[];
actions: ModerationAction[];
priority: number;
enabled: boolean;
}
interface RuleCondition {
field: keyof UnifiedComment | 'author.isVerified' | 'sentiment';
operator: 'equals' | 'contains' | 'matches' | 'gt' | 'lt';
value: string | number | boolean | RegExp;
}
class ModerationEngine {
private rules: ModerationRule[] = [];
addRule(rule: ModerationRule): void {
this.rules.push(rule);
this.rules.sort((a, b) => b.priority - a.priority);
}
async processComment(comment: UnifiedComment): Promise<{
actions: ModerationAction[];
matchedRules: string[];
}> {
const matchedRules: string[] = [];
const actions: Set<ModerationAction> = new Set();
for (const rule of this.rules) {
if (!rule.enabled) continue;
const matches = this.evaluateConditions(comment, rule.conditions);
if (matches) {
matchedRules.push(rule.id);
rule.actions.forEach(action => actions.add(action));
}
}
return {
actions: Array.from(actions),
matchedRules,
};
}
private evaluateConditions(
comment: UnifiedComment,
conditions: RuleCondition[]
): boolean {
return conditions.every(condition => {
const value = this.getFieldValue(comment, condition.field);
return this.evaluateCondition(value, condition.operator, condition.value);
});
}
private getFieldValue(comment: UnifiedComment, field: string): unknown {
const parts = field.split('.');
let value: unknown = comment;
for (const part of parts) {
if (value && typeof value === 'object') {
value = (value as Record<string, unknown>)[part];
} else {
return undefined;
}
}
return value;
}
private evaluateCondition(
fieldValue: unknown,
operator: RuleCondition['operator'],
ruleValue: RuleCondition['value']
): boolean {
switch (operator) {
case 'equals':
return fieldValue === ruleValue;
case 'contains':
return typeof fieldValue === 'string' &&
fieldValue.toLowerCase().includes(String(ruleValue).toLowerCase());
case 'matches':
return typeof fieldValue === 'string' &&
(ruleValue instanceof RegExp ? ruleValue : new RegExp(String(ruleValue))).test(fieldValue);
case 'gt':
return typeof fieldValue === 'number' && fieldValue > Number(ruleValue);
case 'lt':
return typeof fieldValue === 'number' && fieldValue < Number(ruleValue);
default:
return false;
}
}
}
Example rules you might configure:
const moderationEngine = new ModerationEngine();
// Auto-hide comments with profanity
moderationEngine.addRule({
id: 'profanity-filter',
name: 'Hide Profanity',
conditions: [
{ field: 'text', operator: 'matches', value: /\b(badword1|badword2)\b/i }
],
actions: ['hide', 'flag'],
priority: 100,
enabled: true,
});
// Escalate negative sentiment from verified users
moderationEngine.addRule({
id: 'verified-negative',
name: 'Escalate Verified Negative',
conditions: [
{ field: 'author.isVerified', operator: 'equals', value: true },
{ field: 'sentiment', operator: 'equals', value: 'negative' }
],
actions: ['escalate'],
priority: 90,
enabled: true,
});
// Auto-approve comments from frequent engagers
moderationEngine.addRule({
id: 'trusted-commenter',
name: 'Auto-approve Trusted',
conditions: [
{ field: 'author.id', operator: 'contains', value: 'trusted_list' }
],
actions: ['approve'],
priority: 80,
enabled: true,
});
AI-Powered Sentiment Analysis
Modern comment moderation API implementations leverage AI for sentiment analysis and toxicity detection. You can integrate OpenAI or other providers to classify comments automatically:
interface SentimentResult {
sentiment: 'positive' | 'neutral' | 'negative';
score: number; // -1 to 1
confidence: number; // 0 to 1
toxicity?: number; // 0 to 1
categories?: string[]; // spam, hate, harassment, etc.
}
async function analyzeCommentSentiment(
comment: UnifiedComment,
openaiApiKey: string
): Promise<SentimentResult> {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openaiApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: `Analyze the sentiment of social media comments. Return JSON with:
- sentiment: "positive", "neutral", or "negative"
- score: number from -1 (very negative) to 1 (very positive)
- confidence: number from 0 to 1
- toxicity: number from 0 to 1 (0 = not toxic, 1 = very toxic)
- categories: array of applicable labels from [spam, hate, harassment, threat, self_harm, sexual, none]`
},
{
role: 'user',
content: `Analyze this comment:\n\n"${comment.text}"\n\nContext: This is a ${comment.platform} comment on a brand's post.`
}
],
response_format: { type: 'json_object' },
temperature: 0.1,
}),
});
if (!response.ok) {
throw new Error(`OpenAI API error: ${response.status}`);
}
const data = await response.json();
const result = JSON.parse(data.choices[0].message.content);
return {
sentiment: result.sentiment,
score: result.score,
confidence: result.confidence,
toxicity: result.toxicity,
categories: result.categories?.filter((c: string) => c !== 'none'),
};
}
// Batch processing for efficiency
async function analyzeCommentBatch(
comments: UnifiedComment[],
openaiApiKey: string,
batchSize: number = 10
): Promise<Map<string, SentimentResult>> {
const results = new Map<string, SentimentResult>();
for (let i = 0; i < comments.length; i += batchSize) {
const batch = comments.slice(i, i + batchSize);
const batchPromises = batch.map(async (comment) => {
try {
const sentiment = await analyzeCommentSentiment(comment, openaiApiKey);
return { id: comment.id, sentiment };
} catch (error) {
console.error(`Failed to analyze comment ${comment.id}:`, error);
return { id: comment.id, sentiment: null };
}
});
const batchResults = await Promise.all(batchPromises);
for (const result of batchResults) {
if (result.sentiment) {
results.set(result.id, result.sentiment);
}
}
// Rate limiting between batches
if (i + batchSize < comments.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
Note: For high-volume scenarios, consider using dedicated moderation APIs like Perspective API (free for moderate usage) or AWS Comprehend, which are optimized for this use case and more cost-effective than GPT-4 for simple classification.
Queue-Based Processing Architecture
To automate social media comments at scale, you need asynchronous processing. A queue-based architecture decouples comment ingestion from processing, allowing you to handle traffic spikes gracefully.
interface CommentJob {
type: 'fetch' | 'analyze' | 'moderate' | 'respond';
commentId?: string;
accountId: string;
platform: CommentsPlatform;
payload: Record<string, unknown>;
attempts: number;
maxAttempts: number;
createdAt: Date;
scheduledFor: Date;
}
class CommentProcessingQueue {
private queue: CommentJob[] = [];
private processing: boolean = false;
private concurrency: number = 5;
async enqueue(job: Omit<CommentJob, 'attempts' | 'createdAt'>): Promise<void> {
this.queue.push({
...job,
attempts: 0,
createdAt: new Date(),
});
this.queue.sort((a, b) =>
a.scheduledFor.getTime() - b.scheduledFor.getTime()
);
if (!this.processing) {
this.processQueue();
}
}
private async processQueue(): Promise<void> {
this.processing = true;
while (this.queue.length > 0) {
const now = new Date();
const readyJobs = this.queue.filter(j => j.scheduledFor <= now);
if (readyJobs.length === 0) {
// Wait for next scheduled job
const nextJob = this.queue[0];
const waitTime = nextJob.scheduledFor.getTime() - now.getTime();
await new Promise(resolve => setTimeout(resolve, Math.min(waitTime, 1000)));
continue;
}
// Process up to concurrency limit
const batch = readyJobs.slice(0, this.concurrency);
await Promise.all(batch.map(async (job) => {
this.queue = this.queue.filter(j => j !== job);
try {
await this.processJob(job);
} catch (error) {
await this.handleJobError(job, error as Error);
}
}));
}
this.processing = false;
}
private async processJob(job: CommentJob): Promise<void> {
job.attempts++;
switch (job.type) {
case 'fetch':
await this.handleFetchJob(job);
break;
case 'analyze':
await this.handleAnalyzeJob(job);
break;
case 'moderate':
await this.handleModerateJob(job);
break;
case 'respond':
await this.handleRespondJob(job);
break;
}
}
private async handleJobError(job: CommentJob, error: Error): Promise<void> {
console.error(`Job ${job.type} failed:`, error.message);
if (job.attempts < job.maxAttempts) {
// Exponential backoff
const delay = Math.pow(2, job.attempts) * 1000;
job.scheduledFor = new Date(Date.now() + delay);
this.queue.push(job);
} else {
// Move to dead letter queue
await this.moveToDeadLetter(job, error);
}
}
private async handleFetchJob(job: CommentJob): Promise<void> {
// Implementation: fetch comments from platform
}
private async handleAnalyzeJob(job: CommentJob): Promise<void> {
// Implementation: run sentiment analysis
}
private async handleModerateJob(job: CommentJob): Promise<void> {
// Implementation: apply moderation rules
}
private async handleRespondJob(job: CommentJob): Promise<void> {
// Implementation: post response to platform
}
private async moveToDeadLetter(job: CommentJob, error: Error): Promise<void> {
// Implementation: store failed job for manual review
}
}
For production systems, replace this in-memory queue with Redis (using BullMQ) or a managed service like AWS SQS.
Performance at Scale
When processing thousands of comments per minute, small inefficiencies compound. Here are critical optimizations:
Deduplication: Comments can appear in multiple fetches, especially when polling frequently.
function deduplicateItems<T>(
items: T[],
keyFn: (item: T) => string
): T[] {
const seen = new Set<string>();
return items.filter((item) => {
const key = keyFn(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// Usage
const uniqueComments = deduplicateItems(
allComments,
(comment) => `${comment.platform}_${comment.platformId}`
);
Cursor-based pagination: Offset pagination becomes slow with large datasets. Use cursors that encode position:
interface PaginationInfo {
hasMore: boolean;
nextCursor: string | null;
totalCount?: number;
}
function paginateWithCursor<T extends { id?: string; _id?: unknown }>(
items: T[],
cursor: string | null,
limit: number,
getTimestamp: (item: T) => string,
getAccountId: (item: T) => string
): { items: T[]; pagination: PaginationInfo } {
let filteredItems = items;
// Cursor format: {timestamp}_{accountId}_{itemId}
if (cursor) {
const [cursorTimestamp, cursorAccountId, cursorItemId] = cursor.split('_');
const cursorTime = new Date(cursorTimestamp).getTime();
filteredItems = items.filter((item) => {
const itemTime = new Date(getTimestamp(item)).getTime();
const itemAccountId = getAccountId(item);
const itemId = item.id || String(item._id);
if (itemTime < cursorTime) return true;
if (itemTime === cursorTime) {
if (itemAccountId > cursorAccountId) return true;
if (itemAccountId === cursorAccountId && itemId > cursorItemId) return true;
}
return false;
});
}
const paginatedItems = filteredItems.slice(0, limit);
const hasMore = filteredItems.length > limit;
let nextCursor: string | null = null;
if (hasMore && paginatedItems.length > 0) {
const lastItem = paginatedItems[paginatedItems.length - 1];
nextCursor = `${getTimestamp(lastItem)}_${getAccountId(lastItem)}_${lastItem.id || lastItem._id}`;
}
return {
items: paginatedItems,
pagination: { hasMore, nextCursor },
};
}
Efficient sorting: Pre-sort by common fields and maintain indexes:
function sortItems<T>(
items: T[],
sortBy: string,
sortOrder: 'asc' | 'desc',
fieldMap: Record<string, (item: T) => unknown>
): T[] {
const getter = fieldMap[sortBy];
if (!getter) return items;
return [...items].sort((a, b) => {
const aVal = getter(a);
const bVal = getter(b);
// Handle dates
if (aVal instanceof Date && bVal instanceof Date) {
return sortOrder === 'asc'
? aVal.getTime() - bVal.getTime()
: bVal.getTime() - aVal.getTime();
}
// Handle numbers
if (typeof aVal === 'number' && typeof bVal === 'number') {
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
}
// Handle strings
if (typeof aVal === 'string' && typeof bVal === 'string') {
return sortOrder === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
}
return 0;
});
}
Team Collaboration Features
Enterprise comment management requires team workflows. Multiple moderators need to work without stepping on each other's toes:
interface CommentAssignment {
commentId: string;
assignedTo: string; // User ID
assignedBy: string;
assignedAt: Date;
status: 'pending' | 'in_progress' | 'completed';
notes?: string;
}
interface ModerationAuditLog {
id: string;
commentId: string;
action: ModerationAction;
performedBy: string;
performedAt: Date;
previousState: Partial<UnifiedComment>;
newState: Partial<UnifiedComment>;
reason?: string;
}
class TeamModerationService {
async assignComment(
commentId: string,
assignTo: string,
assignedBy: string
): Promise<CommentAssignment> {
// Check if already assigned
const existing = await this.getAssignment(commentId);
if (existing && existing.status === 'in_progress') {
throw new Error(`Comment already assigned to ${existing.assignedTo}`);
}
const assignment: CommentAssignment = {
commentId,
assignedTo: assignTo,
assignedBy,
assignedAt: new Date(),
status: 'pending',
};
await this.saveAssignment(assignment);
return assignment;
}
async logAction(
commentId: string,
action: ModerationAction,
userId: string,
previousState: Partial<UnifiedComment>,
newState: Partial<UnifiedComment>,
reason?: string
): Promise<void> {
const log: ModerationAuditLog = {
id: generateId(),
commentId,
action,
performedBy: userId,
performedAt: new Date(),
previousState,
newState,
reason,
};
await this.saveAuditLog(log);
}
async getCommentHistory(commentId: string): Promise<ModerationAuditLog[]> {
return this.findAuditLogs({ commentId });
}
// Abstract methods for storage implementation
protected async getAssignment(commentId: string): Promise<CommentAssignment | null> {
throw new Error('Not implemented');
}
protected async saveAssignment(assignment: CommentAssignment): Promise<void> {
throw new Error('Not implemented');
}
protected async saveAuditLog(log: ModerationAuditLog): Promise<void> {
throw new Error('Not implemented');
}
protected async findAuditLogs(query: { commentId: string }): Promise<ModerationAuditLog[]> {
throw new Error('Not implemented');
}
}
Using Late for Comment Management
Building and maintaining multi-platform comment infrastructure is complex. You need to handle eight different APIs, each with unique authentication flows, rate limits, data formats, and webhook systems. When platforms update their APIs (which happens constantly), your code breaks.
Late provides a unified API for social media comment moderation that handles all this complexity. Instead of building separate integrations for Facebook, Instagram, Twitter, YouTube, LinkedIn, Reddit, Bluesky, and Threads, you use a single endpoint.
Late's comment management features include:
- Unified inbox: All comments from all platforms in one normalized format
- Real-time aggregation: Comments fetched automatically with intelligent polling
- Built-in error handling: Platform outages don't crash your system
- Team collaboration: Assignment, audit logs, and role-based access
- Webhook support: Get notified of new comments instantly
Here's how simple comment fetching becomes with Late:
// Instead of 8 different API integrations...
const response = await fetch('https://api.getlate.dev/v1/inbox/comments', {
headers: {
'Authorization': `Bearer ${LATE_API_KEY}`,
},
});
const { comments, pagination, meta } = await response.json();
// comments: UnifiedComment[] - already normalized
// pagination: { hasMore, nextCursor }
// meta: { accountsQueried, accountsFailed, failedAccounts }
Late handles the platform-specific complexity: OAuth token refresh, rate limit management, webhook verification, and data normalization. You focus on building your moderation workflows and user experience.
Check out Late's documentation to see the full inbox API reference, including filtering by platform, sentiment analysis integration, and bulk moderation actions.
Multi-platform comment management is a solved problem if you use the right tools. Whether you build the infrastructure yourself using the patterns in this guide or leverage Late's unified API, the key is designing for scale from the start: normalized data models, async processing, graceful error handling, and team-friendly workflows.

Miquel is the founder of Late, building the most reliable social media API for developers. Previously built multiple startups and scaled APIs to millions of requests.
View all articlesLearn more about Late with AI
See what AI assistants say about Late API and this topic