Overview
ReddMatch helps founders find potential customers in Reddit discussions by matching their product with the problems people describe. Users can assess the original conversation alongside enriched context, then prepare a reply they review and post themselves.
The engineering challenge was making relevant leads available during project setup while keeping acquisition, processing, and storage affordable. I designed a shared discussion index, reduced embedding storage as it approached capacity, and improved matching and dashboard navigation through feedback calls with users.
The challenge
When I tried a competing product, I had to wait seven days to receive leads. I did not want someone paying for ReddMatch to start with the same wait. They should be able to evaluate relevant conversations as soon as they finish setting up their product.
That meant collecting and preparing discussions before a customer requested them. I needed to make that ongoing work affordable while handling restricted API access, growing storage, and a difficult relevance problem: a conversation about the same topic is not necessarily a good lead.
Product experience
A user starts with their product URL rather than a blank product questionnaire. Information extracted from their website is used to prefill the product description, ideal customers, customer problem, and desired outcome. The user can review and edit those details before creating the project. ReddMatch then searches its global index, so available matches can populate the feed during setup rather than days later.
Each lead pairs the original conversation with enriched context about the author's situation, problem, and desired outcome. This helps users assess the opportunity without having to interpret every discussion from scratch. Users can then generate a reply draft, review it, and copy it to post on Reddit themselves.
Review and edit the product details extracted from your website before creating a project.
The lead view pairs the original discussion with structured context for review.
Architecture
The Next.js application handles the customer experience and application APIs. A separate TypeScript engine acquires and processes discussions in the background. PostgreSQL holds the global post index and project-specific matches, with pgvector handling similarity comparisons.
The engine is an npm-workspaces monorepo with shared processing packages. Workers divide communities between instances, retrieve posts, filter and score them, generate embeddings, and invoke database matching. Each discussion is stored once, with separate project records holding its similarity and user interactions.
Environment variables control worker assignments, request pacing, batch sizes, thresholds, retention, and run intervals. After updates, an authenticated webhook invalidates the web application's lead caches.
Separating enrichment from matching
I validate the extracted product details before prefilling the editable onboarding form. Incoming discussions are scored for intent and enriched with context for lead review. Matching uses a separate representation: the product summary and original post text, rather than the generated lead explanation.
- 1Fetch discussions
- 2Filter & score
- 3Embed posts
- 1Extract product details
- 2Review product details
- 3Embed summary
Built with
- React
- TypeScript
- Next.js
- Node.js
- PostgreSQL
- Prisma
- AWS S3
- AWS CloudFront
- Stripe
- Tailwind CSS
- Shadcn UI
- GitHub
- Supabase
- Clerk
- OpenAI
- Gemini
- Resend
- PostHog
- MDX
Engineering decisions
Global indexing instead of collecting leads on demand
I separated acquisition from the customer's request. Creating a project triggers matching against prepared data, not a new search-and-processing job against Reddit. The same acquisition and embedding work can therefore serve multiple products instead of being repeated for each customer.
The cost is doing work ahead of demand: I collect and store discussions that may never match a paying customer's product. New discussions appear after the next processing run, rather than in real time. A niche outside the indexed communities can still have few or no matches.
On-demand acquisition could target each request more narrowly, but it would put external API delays and processing back into onboarding. I chose immediate access to available matches, accepting the ongoing responsibility of managing coverage, freshness, and storage.
Filter before paying for more processing
I remove repeated Reddit IDs, reject unsuitable content, and check for already-stored posts before sending candidates for paid intent scoring. Only qualifying posts proceed to embedding. This avoids spending model calls on duplicates and obvious noise.
Scoring batches have both item-count and text-size limits, so a few long discussions cannot silently turn a small batch into an oversized request. Intent scores and extracted fields are validated before the results move further through the pipeline.
Keeping workers running through partial failures
A temporary provider failure should not stop discovery for every project. I use request timeouts and bounded retries with exponential backoff and jitter for retryable failures, including rate limits and server errors.
If an acquisition batch exhausts its retries, the worker records the failure and continues with later batches. If an error ends the run, the outer loop reports it and schedules another run instead of terminating the worker. This keeps a partial failure from stopping future discovery.
The wait between runs defaults to three hours and is configurable to balance freshness against processing cost. Stage-level logs help locate failures in the pipeline.
Two temporary scoring failures trigger retries with increasing delays. The batch then succeeds and processing continues.
Run 441 completes indexing, matching and cleanup, then schedules the next processing cycle after a three-hour wait.
Keeping the index recent instead of storing everything
I reject posts outside the configured age window before scoring them, avoiding paid processing for old discussions. A separate cleanup step deletes expired stored posts. Both cutoffs default to seven days in the code. Together, they limit unnecessary processing and storage while keeping the index focused on recent opportunities.
Optimizing vector storage before the database filled up
Global indexing made storage a real operating constraint. When the database approached capacity, I reduced product and post embeddings from 1,536 dimensions to 512, preserving compatible representations for matching.
Keeping the same numeric representation, this reduced storage per embedding by approximately 67% and recovered substantial database capacity. It gave the growing index more room within the existing storage allowance. This reduction applies to the embeddings, not the entire database.
When I reviewed the matches after reducing the dimensions, I did not see a substantial drop in relevance. That made the storage savings a worthwhile trade-off for this stage of the product.
Problems & iterations
Enriching both sides made matches too broad
A founder reported receiving posts about fighting bots, the opposite of what their service offered. The conversations shared relevant terminology but pointed toward the wrong need. That feedback prompted me to investigate what the matching system was actually comparing.
At the time, I was comparing enriched product descriptions with enriched lead descriptions. Rewriting both sides turned specific conversations into broader statements about similar problems. Matches looked related without necessarily being useful.
I changed the inputs asymmetrically: a focused product summary on one side, the original Reddit title and body on the other. The post still receives intent scoring and extracted context, but that generated context is not what goes into its embedding.
Retaining the conversation's specific wording worked better in my review, although false positives remain. Semantic similarity helps find candidates, but it does not establish product fit by itself.


Shared terminology, wrong intent: this person wants to block bot traffic.
Relevant intent: this person is looking for a cost-effective Reddit data-extraction service.
Selected ScrapeBadger results before and after changing the matching inputs.
Changing acquisition without replacing the pipeline
The original Reddit public API integration stopped working for the project, so I switched to ScrapeBadger. I adapted requests and normalized responses into the engine's existing post structure, keeping filtering, scoring, storage, and matching behind that boundary.
Making the dashboard easier to navigate
After shipping, I held one-to-one calls with users to understand their experience with ReddMatch. In one of those conversations, a user described having trouble navigating the dashboard and finding the lead feed.
I made the project switcher more visible, redesigned the project cards, and introduced dedicated project pages instead of the layout that had made the feed difficult to find. These changes focused on making it clearer how to move between projects and reach their leads.
I also added guided tours that highlight the relevant dashboard controls and explain the next step in place. Users can follow the path from choosing a project to opening its lead feed, with a mobile-specific step for opening the sidebar. This complemented the navigation redesign by helping users learn the workflow inside the interface.
That was an important correction: implementing the workflow was not enough if people could not find their way through it. Alongside Ronald's QA, direct conversations with users gave me another way to identify what needed attention.
Guided navigation helps users choose a project and find its lead feed.
Results & next steps
The completed system lets a new customer review available matches during setup instead of waiting for collection to begin. Background processing continues to add conversations, while shared indexing avoids repeating acquisition and embedding work for each project. The storage optimization recovered capacity for the index to grow within the existing allowance.
Matching remains the main technical focus. I am continuing to improve relevance while keeping acquisition and storage affordable, and treating coverage gaps separately from ranking problems. The goal is to make the conversations useful, not simply fill the feed.
