19 KiB
Service Catalog
Detailed documentation for each service in the RedditBot codebase.
Table of Contents
- MonitorService
- ProcessorService
- StrategySelector
- ResponseChecker
- ChatService
- MediaService
- MediaSyncService
- AutoReplyService
- RedditApi
- OpenRouterService
- BrowserClient
- MessagePage
- RateLimiter
MonitorService
File: src/services/monitor.ts
Lines: 72
Responsibility: Discover new image posts across configured subreddits
Purpose
Polls configured subreddits at regular intervals, discovers new image posts, filters out already-processed posts and previously-messaged users, and returns the single newest valid post for processing.
Public Methods
class MonitorService {
constructor(
private redditApi: RedditApi,
private postsRepo: PostsRepository,
private messagesRepo: MessagesRepository,
private config: Config
)
/**
* Discover the next post to process.
* Returns null if no valid posts are available.
*/
discoverNextPost(): Promise<Post | null>
}
Algorithm
- Shuffle subreddit order (randomize polling order each cycle)
- For each subreddit:
- Fetch new posts via
RedditApi.fetchPosts() - Filter: image posts only
- Filter: author not deleted
- Filter: post not already in
processed_posts - Filter: author not previously messaged (or meets re-message criteria)
- Fetch new posts via
- Return the newest valid post across all subreddits
- If no valid posts found, return
null
Dependencies
RedditApi- Fetch posts from RedditPostsRepository- Check for already-processed postsMessagesRepository- Check for previously-messaged usersConfig- Subreddit list, poll interval, target gender
Error Handling
- Skips subreddits that return errors (continues to next)
- Logs errors at
warnlevel - Returns
nullon any unrecoverable error
ProcessorService
File: src/services/processor.ts
Lines: 132
Responsibility: Core processing pipeline for discovered posts
Purpose
Orchestrates the full processing pipeline for a single discovered post: image download, gender detection, AI analysis, strategy selection, message generation, and DM sending.
Public Methods
class ProcessorService {
constructor(
private config: Config,
private redditApi: RedditApi,
private openrouter: OpenRouterService,
private strategySelector: StrategySelector,
private messageSender: MessagePage,
private postsRepo: PostsRepository,
private messagesRepo: MessagesRepository,
private successfulRepo: SuccessfulMessagesRepository,
private logger: Logger
)
/**
* Process a single discovered post through the full pipeline.
*/
processPost(post: Post): Promise<void>
}
Pipeline Steps
1. Download image
└─► ImageService.downloadImage(url)
└─► Validate MIME type (image/jpeg, image/png, image/webp)
└─► Validate file size
└─► Convert to base64
2. Quick gender detection (cost-saving pre-filter)
└─► OpenRouter.detectGenderOnly(image)
└─► Skip if gender doesn't match targetGender
3. Select A/B strategy variant
└─► StrategySelector.selectStrategy(subreddit, author)
4. Full AI analysis
└─► OpenRouter.analyzeImage(image)
└─► Detect gender, describe image
└─► Handle refusal (mark post, try next)
5. Generate message
└─► OpenRouter.generateMessage(analysis, title, examples, strategy)
└─► Few-shot examples from SuccessfulMessagesRepository
6. Send DM
└─► MessagePage.sendDM(author, "Stunning", message)
└─► Handle failure (log, don't mark as sent)
7. Record in database
└─► Mark post as processed
└─► Mark message_sent = 1 if successful
└─► Store generated_message for reference
Error Handling
- AI refusal: Post is marked as processed with error, skipped
- Image download failure: Post is marked with error, skipped
- DM send failure: Post is marked as not sent, logged
- All errors logged with context (post ID, subreddit, author)
Cost Optimization
- Quick gender detection costs less than full analysis
- Non-matching genders are skipped before expensive analysis
- AI refusal detection prevents wasted tokens on rejected prompts
StrategySelector
File: src/services/strategy-selector.ts
Lines: 101
Responsibility: A/B testing with explore/exploit algorithm
Purpose
Selects message variants (strategies) for each post using an explore/exploit algorithm based on historical response rates. Optimizes for maximum response rate over time while continuing to explore under-tested variants.
Public Methods
class StrategySelector {
constructor(
private config: Config,
private messagesRepo: MessagesRepository
)
/**
* Select the best strategy variant for a given subreddit/author.
*/
selectStrategy(subreddit: string, author: string): string
}
Algorithm
-
Get strategy stats from
MessagesRepository:- Response count per variant
- Sent count per variant
- Response rate per variant
-
Apply minimum sample threshold:
- If a variant has fewer than
minSamplessends, treat it as untested - Untested variants are excluded from exploitation
- If a variant has fewer than
-
Explore vs Exploit decision:
- With probability
explorationRate, select a random variant (explore) - Otherwise, select the variant with the highest response rate (exploit)
- With probability
-
Fallback:
- If no variants have enough data, select randomly from enabled variants
Configuration
strategyVariants:
- name: "variant_a"
prompt: "Compliment-based message..."
enabled: true
- name: "variant_b"
prompt: "Question-based message..."
enabled: true
Dependencies
Config- Strategy variants, exploration rate, min samplesMessagesRepository- Response rate statistics
ResponseChecker
File: src/services/response-checker.ts
Lines: 179
Responsibility: Monitor inbox for user replies
Purpose
Background service that navigates the Reddit inbox via Playwright, identifies responses to bot-sent messages, updates the database, and stores successful message/response pairs for few-shot learning.
Public Methods
class ResponseCheckerService {
constructor(
private browser: BrowserClient,
private messagesRepo: MessagesRepository,
private successfulRepo: SuccessfulMessagesRepository,
private logger: Logger
)
/**
* Check inbox for responses to sent messages.
*/
checkResponses(): Promise<void>
}
Algorithm
- Navigate to
/message/inboxvia Playwright - Parse inbox items (messages, conversations)
- For each response item:
- Extract recipient username and message text
- Match to
sent_messagesby recipient + timestamp proximity - Update
got_response = 1andresponse_checked_at - Store the message/response pair in
successful_messages
- Update strategy statistics (response rates)
Dependencies
BrowserClient- Playwright browser contextMessagesRepository- Update response flagsSuccessfulMessagesRepository- Store learning pairsLogger- Activity logging
ChatService
File: src/services/chat.ts
Lines: 1123+
Responsibility: Reddit chat scraping, messaging, and conversation management
Purpose
The most complex service in the codebase. Manages Reddit chat interactions including scraping active conversations, loading conversation history, sending messages with human-like delays, detecting upsell/spam, and managing conversation caches.
Public Methods
class ChatService {
constructor(
private browser: BrowserClient,
private config: Config,
private logger: Logger
)
/** Fetch active conversations from Reddit */
fetchChats(): Promise<ChatSummary[]>
/** Load full conversation history */
loadConversation(chatId: string): Promise<Conversation>
/** Send a message in a conversation */
sendMessage(chatId: string, text: string): Promise<boolean>
/** Get cached conversations */
getCachedConversations(): Conversation[]
/** Check for new messages in conversations */
checkForUpdates(): Promise<Conversation[]>
/** Detect upsell/spam in a message */
detectUpsell(message: string): boolean
}
Shadow DOM Handling
Reddit uses custom faceplate-* web components with shadow DOM. Standard CSS selectors cannot pierce these boundaries. The service uses JavaScript injection via page.evaluate():
// Traverse shadow roots to find chat elements
function traverseShadowRoots(root) {
const elements = root.querySelectorAll('.some-class');
const shadowChildren = root.querySelectorAll('*::shadow, ::deep');
// ... recursive traversal
}
Conversation Cache
Conversations are cached in two layers:
- In-memory: Fast access for active conversations
- Disk JSON: Persistence across restarts at
data/chats/
Cache structure:
{
"chatId": "abc123",
"participants": ["user1", "bot"],
"messages": [
{ "author": "user1", "text": "Hey!", "timestamp": 1234567890 }
],
"lastUpdated": 1234567890,
"hasNewMessages": false
}
Human-Like Messaging
Messages are sent with:
- Typing delay simulation (random delay before "typing" starts)
- Variable typing speed
- Random delays between messages
- Delivery verification
Known Issues
- Hardcoded username:
myUsername = 'Upset_Marsupial1006'is hardcoded in JS evaluation scripts. Should be parameterized fromConfig. - Size: 1123+ lines suggests this should be split into smaller services.
Dependencies
BrowserClient- Playwright browser contextConfig- Rate limits, username (should be), check intervalsLogger- Activity logging
MediaService
File: src/services/media.ts
Lines: 254
Responsibility: Media download, caching, and indexing
Purpose
Downloads images from chat conversations, resolves Matrix blurred thumbnails to full resolution, stores files locally, and maintains a deduplicated index.
Public Methods
class MediaService {
constructor(
private config: Config,
private browser: BrowserClient,
private logger: Logger
)
/** Download media from a conversation */
downloadMedia(chatId: string): Promise<string[]>
/** Get media index */
getIndex(): MediaIndex
/** Get a specific media file path */
getPath(hash: string): string | null
}
Deduplication
Files are deduplicated using SHA-1 hashing:
SHA-1(image_bytes) → data/media/{hash}.{ext}
The index maps hashes to file paths:
{
"abc123def456": "abc123def456.jpg",
"789xyz012abc": "789xyz012abc.png"
}
Matrix Thumbnail Resolution
Reddit's Matrix system uses blurred placeholder thumbnails. The service resolves these to full resolution by:
- Detecting Matrix thumbnail URLs
- Extracting the full-resolution URL from the response
- Downloading the full-resolution image
Dependencies
Config- Media directory pathBrowserClient- Playwright for Matrix resolutionLogger- Download activity
MediaSyncService
File: src/services/media-sync.ts
Lines: 119
Responsibility: Periodic background media synchronization
Purpose
Runs periodic background syncs to download media from chat conversations. Supports both incremental (top 10 chats) and full (all conversations) sync modes.
Public Methods
class MediaSyncService {
constructor(
private config: Config,
private mediaService: MediaService,
private chatService: ChatService,
private logger: Logger
)
/** Start periodic sync */
start(): void
/** Stop periodic sync */
stop(): void
/** Trigger incremental sync */
syncIncremental(): Promise<void>
/** Trigger full sync */
syncFull(): Promise<void>
/** Get sync status */
getStatus(): SyncStatus
}
Sync Modes
| Mode | Description | Use Case |
|---|---|---|
| Incremental | Top 10 most recent conversations | Regular background sync |
| Full | All conversations | Initial setup, catch-up |
Dependencies
Config- Sync interval, enabled flagMediaService- Actual download logicChatService- Conversation listLogger- Sync progress
AutoReplyService
File: src/services/auto-reply.ts
Lines: 199
Responsibility: Automated reply to chat conversations
Purpose
Monitors cached conversations for unread messages, detects upsell/spam, generates AI reply suggestions, and either auto-sends or marks as pending for manual approval.
Public Methods
class AutoReplyService {
constructor(
private config: Config,
private chatService: ChatService,
private openrouter: OpenRouterService,
private logger: Logger
)
/** Start auto-reply checking */
start(): void
/** Stop auto-reply checking */
stop(): void
/** Check for unread messages and generate replies */
checkAndReply(): Promise<void>
}
Workflow
1. Check cached conversations for unread messages
│
├─► Skip if no new messages
│
├─► Detect upsell/spam keywords
│ └─► Flag conversation, skip reply
│
├─► Generate AI reply suggestion
│ └─► OpenRouter.generateReply(conversation history)
│
├─► If autoSend: send reply immediately
│ └─► ChatService.sendMessage()
│
└─► If !autoSend: mark as pending
└─► Dashboard shows pending replies for approval
Dependencies
Config- Enabled flag, autoSend flag, check intervalChatService- Conversation access, message sendingOpenRouterService- Reply generationLogger- Activity logging
RedditApi
File: src/reddit/api.ts
Lines: 233
Responsibility: Reddit API client
Purpose
Axios-based client for interacting with Reddit's public API. Handles post fetching, user profile checking, and authenticated requests using Playwright browser cookies.
Public Methods
class RedditApi {
constructor(
private browser: BrowserClient,
private logger: Logger
)
/** Fetch new posts from a subreddit */
fetchPosts(subreddit: string, limit?: number): Promise<Post[]>
/** Check if a user is a seller/spam */
checkUserProfile(username: string): Promise<UserProfile>
/** Make authenticated request using browser cookies */
authenticatedRequest(url: string): Promise<any>
}
Image URL Resolution
Fallback chain for extracting image URLs from post data:
1. post.preview.reddits[0].url (Reddit CDN preview)
2. post.url (i.redd.it direct)
3. post.thumbnail (if not self/spam)
4. Scan post.selftext for imgur/image links
Seller/Spam Detection
Checks user profile for keywords:
- "seller"
- "onlyfans"
- "link in bio"
- Other configured
sellerKeywords
Dependencies
BrowserClient- Browser cookies for authenticationLogger- Request logging
OpenRouterService
File: src/ai/openrouter.ts
Lines: 307
Responsibility: OpenRouter AI client
Purpose
Client for the OpenRouter API (OpenAI-compatible SDK). Provides image analysis, message generation, reply suggestions, and upsell detection.
Public Methods
class OpenRouterService {
constructor(private config: Config, private logger: Logger)
/** Full image analysis with model fallback */
analyzeImage(imageBase64: string): Promise<ImageAnalysis>
/** Quick gender detection (cost-saving) */
detectGenderOnly(imageBase64: string): Promise<string>
/** Generate DM message */
generateMessage(
imageAnalysis: ImageAnalysis,
postTitle: string,
strategy: string
): Promise<string>
/** Generate chat reply */
generateReply(conversationHistory: Message[]): Promise<string>
/** Detect upsell/spam keywords */
detectUpsell(message: string): boolean
}
Model Fallback Chain
Primary model (config.openrouter.model)
│
└─► If refusal detected:
│
└─► Try each fallback model (config.openrouter.fallbackModels)
│
└─► If all fail: throw error
Refusal Detection
Heuristic patterns to detect when AI models refuse to process prompts:
const refusalPatterns = [
/cannot.*analyze/i,
/unable.*process/i,
/against.*policy/i,
/cannot.*view/i,
// ...
];
Few-Shot Learning
generateMessage() retrieves successful message/response pairs from the database and includes them as examples in the prompt:
"Here are examples of messages that got responses:
- Example 1: {original_message} → {response_snippet}
- Example 2: ...
Generate a similar message for this image..."
Dependencies
Config- API key, model selection, promptsMessagesRepository- Successful examples for few-shot learning
BrowserClient
File: src/browser/client.ts
Lines: 302
Responsibility: Playwright persistent context manager
Purpose
Manages the headless Playwright context used after an extension dump is imported. It accepts Reddit cookies, verifies account identity, and supplies authenticated context to the remaining browser-backed operations. It never performs login.
Public Methods
class BrowserClient {
constructor(private config: Config, private logger: Logger)
/** Initialize browser context */
initialize(): Promise<void>
/** Verify browser session is active */
verifySession(): Promise<boolean>
/** Import Reddit cookies from a validated extension dump */
importRedditCookies(cookies: Cookie[]): Promise<void>
/** Get page context for operations */
getPage(): Promise<Page>
/** Close browser */
close(): Promise<void>
}
Import Flow
The dashboard validates a combined extension dump, verifies Matrix with whoami, imports Reddit cookies, verifies Reddit identity, and only then marks the shared account gate authenticated.
Session Persistence
- Cookies and storage are saved to
config.browser.userDataDir - Subsequent launches restore the session automatically
- Session expiry pauses account work until a fresh extension dump is imported
Dependencies
Config- Persistent session directoryLogger- Import verification progress and errors
RateLimiter
File: src/utils/rate-limiter.ts
Lines: 114
Responsibility: Token-bucket rate limiting
Purpose
Enforces rate limits on bot actions (primarily message sending) to avoid detection and bans. Uses a combination of minimum delay, random jitter, and hourly caps.
Public Methods
class RateLimiter {
constructor(config: RateLimitConfig)
/** Wait until a slot is available */
waitForSlot(): Promise<void>
/** Get remaining time until next slot */
getTimeUntilSlot(): number
/** Get current hourly count */
getHourlyCount(): number
/** Reset hourly counter */
resetHourlyCounter(): void
}
Rate Limiting Strategy
1. Check hourly cap:
└─► If maxMessagesPerHour reached: wait until hour rolls over
2. Enforce minimum delay:
└─► lastActionTime + minDelayMs + random(0, jitterMs)
3. Track action timestamp
4. Increment hourly counter
Dependencies
Config- Rate limit settingsp-queue- Concurrency control