Category: Social Publishing
Integration type: Tenant OAuth (stored in integrations table)
External API: Reddit OAuth2 API v1
Purpose
Reddit integration enables community engagement and organic brand presence in relevant subreddits. Use cases:
- Social Post Writer — generates Reddit-native posts (Ask Me Anything prompts, resource shares, discussions) that match subreddit culture
- Topic Researcher — reads subreddit discussions to surface trending topics in a client’s niche
- Social Publisher — posts approved content to the tenant’s Reddit account
Reddit requires a distinctly different content strategy to other platforms — promotional content is actively penalised by communities. The agent’s system prompt is adjusted for Reddit’s culture (value-first, community-native).
Important: Reddit posts must be submitted under the tenant’s own Reddit account (not the platform’s). The platform facilitates posting on behalf of the tenant; it does not maintain a shared Reddit account.
Config Structure
OAuth flow
scope: submit,read,identity
Stored in integrations:
provider: 'reddit'
api_key: encrypt(refresh_token)
metadata: {
redditUsername: 'acme_plumbing_official',
accessToken: string,
tokenExpiresAt: string, // Reddit tokens expire after 1 hour
}Reddit access tokens are short-lived (1 hour). The tool layer refreshes using the stored refresh token before each call.
Platform app credentials (env vars)
REDDIT_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxx
REDDIT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
REDDIT_USER_AGENT=Leadmetrics/1.0 by /u/leadmetrics_botThe User-Agent header is required by Reddit’s API. It must follow the format platform/version by /u/username. Using a generic UA results in rate limiting or bans.
Integration Pattern
Token refresh + post submission
class RedditTool {
private baseUrl = 'https://oauth.reddit.com';
constructor(
private clientId: string,
private clientSecret: string,
private refreshToken: string,
private userAgent: string,
) {}
private async getAccessToken(): Promise<string> {
const response = await axios.post(
'https://www.reddit.com/api/v1/access_token',
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
}),
{
auth: { username: this.clientId, password: this.clientSecret },
headers: { 'User-Agent': this.userAgent },
},
);
return response.data.access_token;
}
private async headers() {
const token = await this.getAccessToken();
return {
Authorization: `bearer ${token}`,
'User-Agent': this.userAgent,
};
}
async submitPost(options: {
subreddit: string; // Without r/ prefix, e.g. "plumbing"
title: string; // Max 300 characters
kind: 'self' | 'link';
text?: string; // For kind='self' (text post)
url?: string; // For kind='link'
flair?: string; // Flair text (if subreddit requires it)
nsfw?: boolean;
spoiler?: boolean;
}): Promise<{ postId: string; postUrl: string }> {
const body: any = {
sr: options.subreddit,
kind: options.kind,
title: options.title,
resubmit: false,
nsfw: options.nsfw ?? false,
spoiler: options.spoiler ?? false,
};
if (options.kind === 'self') body.text = options.text ?? '';
if (options.kind === 'link') body.url = options.url;
if (options.flair) body.flair_text = options.flair;
const response = await axios.post(
`${this.baseUrl}/api/submit`,
new URLSearchParams(body),
{ headers: await this.headers() },
);
const json = response.data.json;
if (json.errors?.length) {
throw new Error(`Reddit submit error: ${json.errors.map((e: any) => e[1]).join(', ')}`);
}
const postData = json.data;
return {
postId: postData.id,
postUrl: postData.url,
};
}
async getSubredditInfo(subreddit: string): Promise<SubredditInfo> {
const response = await axios.get(
`${this.baseUrl}/r/${subreddit}/about.json`,
{ headers: await this.headers() },
);
const data = response.data.data;
return {
name: data.display_name,
subscribers: data.subscribers,
activeUsers: data.active_user_count,
rules: data.rules ?? [],
flairRequired: data.link_flair_required ?? false,
description: data.public_description,
};
}
async getHotPosts(subreddit: string, limit = 10): Promise<RedditPost[]> {
const response = await axios.get(
`${this.baseUrl}/r/${subreddit}/hot.json`,
{
headers: await this.headers(),
params: { limit },
},
);
return response.data.data.children.map((c: any) => ({
id: c.data.id,
title: c.data.title,
score: c.data.score,
comments: c.data.num_comments,
author: c.data.author,
url: c.data.url,
selftext: c.data.selftext,
flair: c.data.link_flair_text,
}));
}
async verify(): Promise<void> {
const response = await axios.get(`${this.baseUrl}/api/v1/me`, {
headers: await this.headers(),
});
if (!response.data.name) throw new Error('Reddit authentication failed');
}
}Topic research from Reddit
The Topic Researcher agent can pull hot posts from relevant subreddits to discover trending topics:
// Inside topic-researcher worker
const subreddits = tenantProfile.relevantSubreddits ?? ['business', clientNiche];
const allPosts = await Promise.all(
subreddits.map(sub => redditTool.getHotPosts(sub, 20)),
);
// Flatten and pass titles to agent as topic inspiration context
const topicContext = allPosts.flat().map(p => `- ${p.title} (${p.score} upvotes)`).join('\n');Reddit Rate Limits
| Limit | Value |
|---|---|
| OAuth API calls | 60 requests/minute per OAuth client |
| Post submissions | 1 post per 10 minutes per account |
| Account karma threshold | Some subreddits require minimum karma — check rules |
The Social Calendar Planner caps Reddit at 2–4 posts per week per account to avoid appearing spammy. Each submission is reviewed in HITL before posting.
Test Cases
Unit tests (packages/tools/src/reddit.test.ts)
| Test | Approach |
|---|---|
submitPost() POSTs to /api/submit with form-encoded body | Mock axios.post; assert URLSearchParams body and Authorization header |
submitPost() throws when json.errors non-empty | Mock { json: { errors: [['SUBREDDIT_NOEXIST', 'subreddit not found']] } } |
submitPost() returns postId and postUrl from response | Mock { json: { data: { id: 'abc', url: 'https://reddit.com/r/...' } } } |
getSubredditInfo() parses subscriber count | Mock response; assert numeric subscribers |
getHotPosts() maps posts correctly | Mock children array; assert titles and scores |
| Token refresh called before each request | Assert getAccessToken called; assert Authorization: bearer <token> |
verify() throws on auth failure | Mock 401; assert throws |
Related
- Facebook Provider — organic Facebook posting
- LinkedIn Provider — organic LinkedIn posting
- Social Post Writer Agent — generates platform-native copy
- Topic Researcher Agent — uses Reddit for trend discovery
- Tool Integration Layer — OAuth management