Vibecode Rankhog
track this build4 phases, 12 steps, beginner friendly0%The code here is the easy half, and it is worth building. A script that watches subreddits, keeps the threads already ranking in Google, and drafts a reply against the sub's rules is a one-sitting build that will genuinely find you the conversations worth joining. The gap is what happens next. No agent writes you a Reddit account with a year of comment history in the subs you care about, and Reddit's spam enforcement is aimed exactly at accounts that show up new and start mentioning a product. A shadowban is silent: your comments look live to you and are invisible to everyone else, so you learn about it weeks later. Build the finder, then spend the weeks yourself, or pay someone to have already spent them.
You are building a lean indie version of Rankhog. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Rankhog · indie build A local opportunity finder whose hard problems are being a well-behaved API client without hard-coding a rate you cannot verify, spending a paid search budget only on threads worth checking, and drafting replies that answer a question rather than advertising. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node 22 command line sweep plus a review page on localhost | the sweep is a scheduled job, the review is a reading task | | Reddit access | Registered API credentials, a descriptive User-Agent, pacing taken from the response headers | unidentified traffic at a guessed rate is how a client gets blocked | | Storage | SQLite where a dismissed thread is dismissed permanently | the second time you see a thread you already rejected, you stop opening the tool | | Ranking filter | A search API behind a per-sweep query budget and a cache | every query costs money and the same keyword returns the same page all week | | Drafting | One model call per candidate, fed the subreddit's own rules, output stored and never sent | a draft is a starting point, not an outbox | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the sweep and the local review page. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Registered Reddit API credentials** · free for personal, non-commercial use; commercial use needs separate approval - Why: The API expects an identified client, and the terms you agree to when registering are the ones that govern what you may do with the data. - Get it: Create an application in your Reddit preferences, note the client identifier and secret, and read the current data API terms and developer guidance before writing the client. - Verify: An authenticated search returns results and the response carries rate limit headers you can read - [ ] **A search results API key** · a few pounds per thousand queries on most providers - Why: The ranking filter is the thing that separates a thread worth answering from one nobody will ever see, and it is the only per-query cost in the build. - Get it: Any search API with published per-query pricing and a spend cap you can set. - Verify: One keyword lookup returns results and the account shows the queries used - [ ] **An API key for a language model** · pennies per sweep at a few dozen drafts - Why: Drafting is the other external cost, and it should have its own limit rather than sharing one. - Get it: Create a key with its own spend limit. - Verify: A draft returns and the token usage is recorded - [ ] **A Reddit account with real history in your target subreddits** · free, and expensive in time - Why: This is the part the tool cannot give you, and the reason the paid product has a business. Without it the best draft in the world gets removed. - Get it: Comment usefully, without mentioning anything you sell, for several weeks. There is no shortcut and buying one is worse than nothing. - Verify: You have posted ordinary comments in each target subreddit over multiple weeks - [ ] **Your query budget, promotional cap and participation ratio** · free - Why: Each is enforced by code and asserted by a test, and choosing them after a bad week means choosing whatever already happened. - Get it: Write down the paid queries per sweep, the promotional replies per subreddit per week, and the ordinary to promotional ratio below which the tool stops suggesting promotion. - Verify: All three are in the configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. - account age, karma, and comment history in the subs that matter - the warm-up: weeks of ordinary participation before you can mention a product - posting through a real browser session rather than the API, which is what keeps accounts unflagged - per-subreddit rule knowledge and pacing judgment - shared accounts across a team, and the managed service If one of those is essential to you, that is the reason to keep paying for Rankhog, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Rankhog The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a Reddit opportunity finder that drafts a reply and leaves the posting to you to replace Rankhog. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this build stops where it does. Nothing here logs into Reddit, comments, votes, or sends a message. Not through a headless browser, not with a stored session cookie, not 'only on my own account'. Automated promotion is exactly what the platform's spam enforcement exists to catch, and the punishment is silent: a shadowbanned account's comments look live to the person who wrote them and are invisible to everybody else, so you carry on posting into nothing for weeks. This tool finds threads and drafts replies. You read the draft, you edit it, you post it yourself. Which means the first deliverable is not code. Spend a few weeks commenting usefully in the subreddits you care about before you mention anything you sell, and build this in the meantime. ### Stack (fixed, do not substitute) - Runtime: A Node 22 command line sweep plus a review page on localhost · the sweep is a scheduled job, the review is a reading task - Reddit access: Registered API credentials, a descriptive User-Agent, pacing taken from the response headers · unidentified traffic at a guessed rate is how a client gets blocked - Storage: SQLite where a dismissed thread is dismissed permanently · the second time you see a thread you already rejected, you stop opening the tool - Ranking filter: A search API behind a per-sweep query budget and a cache · every query costs money and the same keyword returns the same page all week - Drafting: One model call per candidate, fed the subreddit's own rules, output stored and never sent · a draft is a starting point, not an outbox ### Data model (create this before Phase 1) - keywords(id, text, subreddits_json, enabled, added_at) - threads(id, remote_id, subreddit, title, permalink, author, created_at, score, num_comments, fetched_at) - matches(id, thread_id, keyword_id, matched_at, state) · state is new, ranked, drafted, dismissed or posted_by_me - serp_cache(keyword_id, checked_at, results_json, queries_used) · one lookup per keyword per window, reused by every match - sub_rules(subreddit, rules_json, fetched_at, self_promo_summary) · pulled from the subreddit's own rules endpoint, refreshed on a schedule - drafts(match_id, model, prompt_version, text, created_at, edited_text, copied_at) - participation(subreddit, week, ordinary_comments, promotional_comments) · you fill this in, and the tool refuses to suggest more promotion when the ratio is wrong - Dismissal is permanent. A thread you rejected never appears again, because a finder that repeats itself is noise. ### Phase 1 · A client the platform can identify and slow down Build: Register an application and use its credentials. Send a User-Agent naming the tool, a version and a way to reach you. Read the rate limit headers on every response and sleep when the remaining allowance runs low. Treat a 429 as evidence your pacing is wrong, not as something to retry through. Run one sweep at a time. Trap: Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. Done when: No requests-per-minute constant exists anywhere in the code, verified by review; pacing comes from response headers. The User-Agent names the tool, a version and a contact, following the platform's current published guidance. A 429 halts the sweep, records the reset time and resumes afterwards rather than retrying immediately, tested with a fake server. Credentials live in the environment and never appear in a log line, tested. Do not build yet: No keyword sweep, ranking filter, drafting or interface. ### Phase 2 · The sweep, and the pile you already rejected Build: Search each enabled keyword across its subreddits, store threads with age, score and comment count, and record a match per keyword. Apply cheap filters first: thread age, minimum score, and whether the question has already been answered well in the top comments. Dismissal writes a permanent state and the sweep never selects it again. Trap: Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. Done when: A dismissed thread never appears in a later sweep, tested across two runs. Cheap filters run before anything paid, verified by counting the paid calls in a sweep with all candidates filtered out. Each keyword reports how many candidates it produced and how many survived, so a noisy keyword is visible. Re-running a sweep immediately produces no duplicate matches. Do not build yet: No ranking filter, drafting or costs yet. ### Phase 3 · Only the threads people actually land on Build: For each keyword that survived the filters, look up the search results once per window and cache them. Keep matches whose thread appears in those results. Count queries against a per-sweep budget and stop the sweep when it is reached, reporting what was left unchecked rather than quietly spending more. Trap: Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. Done when: A sweep with twenty matches across four keywords performs at most four paid lookups, asserted by counting calls. Reaching the budget stops the sweep and reports which keywords were unchecked, tested by setting the budget to one. The cache is honoured within its window and expires after it, tested with a clock. The cost of the last sweep is shown in the interface, not only in a log. Do not build yet: No drafting or pacing counters yet. ### Phase 4 · Drafts that answer the question Build: Fetch each subreddit's rules and summarise the self-promotion position. Draft with the thread text and those rules in the prompt, instructed to answer the question in the first paragraph and to mention the product last, briefly, and only where it genuinely fits. Where the rules prohibit promotion, the draft says so and mentions nothing. Store the draft; never send it. Trap: A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. Done when: A draft whose final paragraph is deleted still answers the question, checked as an explicit property before display. A subreddit whose rules prohibit self-promotion produces a draft mentioning no product and says which rule applied. Rules are fetched from the subreddit and shown beside the draft, with their fetch time. Nothing in the codebase can post, comment, vote or message, verified by review and stated in the README. Do not build yet: No pacing counters or export yet. ### Phase 5 · The pace that keeps the account alive Build: Record ordinary comments and promotional comments per subreddit per week, entered by you. Cap suggested promotional drafts per subreddit per week and hide them when the ratio of ordinary to promotional participation falls below your stated line. Show account age and week count as a warm-up progress view. Export matches, drafts and history as CSV and JSON. Trap: The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. Done when: Exceeding the weekly promotional cap for a subreddit hides further promotional drafts and states why, tested. The participation ratio is entered by you and a subreddit below the line shows only non-promotional drafts. The warm-up view shows account age and weeks of ordinary participation per subreddit. The export re-imports into an empty instance with matches, drafts and dismissals intact. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that posts, comments, votes, messages or logs in on your behalf, including a headless browser driving your own session. This is the line the whole build is organised around. - Buying, renting or running multiple accounts, which is against the platform's rules and is the fastest route to losing all of them. - Any promise that a thread will be read or cited by an AI answer engine. Nobody can commit to that, and a feature implying it is selling a hope. - Commercial or resale use of the platform's data, which needs separate written approval under the current data API terms; read them before you build anything beyond personal use. ### README must contain - That nothing in this tool posts, comments, votes or logs into Reddit, and why that line exists - That a shadowban is silent, so automated promotion fails invisibly for weeks - That pacing comes from the API's own rate limit headers and no rate constant exists in the code - The cost of one sweep, the query budget, and where the budget stops - That you should spend a few weeks commenting normally in your target subreddits before mentioning anything you sell ===== AGENTS.md ===== # Agent instructions · Rankhog indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Node 22 command line sweep plus a review page on localhost, Registered API credentials, a descriptive User-Agent, pacing taken from the response headers, SQLite where a dismissed thread is dismissed permanently, A search API behind a per-sweep query budget and a cache, One model call per candidate, fed the subreddit's own rules, output stored and never sent. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ===== BUILD_PLAN.md ===== # Build plan · Rankhog A local opportunity finder whose hard problems are being a well-behaved API client without hard-coding a rate you cannot verify, spending a paid search budget only on threads worth checking, and drafting replies that answer a question rather than advertising. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A client the platform can identify and slow down Every request is authenticated, identifiable, and paced by what the server says rather than by a number you typed. ### Steps 1. Authenticate with registered credentials Read the current terms before you write the client, not after it is blocked. Files: `src/reddit/client.js`, `.env.example` 2. Pace from the response headers The server already tells you the answer on every reply. Files: `src/reddit/pacing.js`, `tests/pacing.test.js` 3. Halt and resume on a rate limit response A retry loop through a 429 is the behaviour that earns a block. Files: `src/reddit/client.js` ### Done when - [ ] No rate constant exists in the code - [ ] The User-Agent names tool, version and contact - [ ] A 429 halts and resumes at the reset time - [ ] Credentials never reach a log ### Watch out - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. ## Phase 2 · The sweep, and the pile you already rejected A sweep turns keywords into candidate threads without ever resurfacing one you dismissed. ### Steps 1. Filter on age, score and answeredness first Everything free happens before anything billed. Files: `src/filters.js`, `tests/filters.test.js` 2. Make dismissal permanent and visible The dismissed pile is what makes the list readable on day ten. Files: `src/matches.js` 3. Report candidates per keyword A keyword that only produces noise should be deleted, not endured. Files: `src/sweep.js` ### Done when - [ ] Dismissed threads never return - [ ] Free filters run before paid calls - [ ] Each keyword reports its yield - [ ] A repeat sweep creates no duplicates ### Watch out - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. ## Phase 3 · Only the threads people actually land on A paid search lookup happens once per keyword per window and the budget is a hard stop. ### Steps 1. Look up per keyword, not per thread The ranking question belongs to the keyword. Files: `src/serp.js`, `tests/serp-cache.test.js` 2. Make the budget a hard stop A soft budget is a bill with an apology attached. Files: `src/budget.js` 3. Show the sweep's cost in the interface A cost you can see is a cost you can decide about. Files: `src/views/sweep.ejs` ### Done when - [ ] One paid lookup per keyword per window - [ ] The budget stops the sweep and reports the remainder - [ ] The cache expires with its window - [ ] The last sweep's cost is visible ### Watch out - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. ## Phase 4 · Drafts that answer the question Every draft opens with a real answer and respects the subreddit's own rules about promotion. ### Steps 1. Put the subreddit's rules in the prompt And show them beside the draft, with the fetch time. Files: `src/rules.js`, `src/draft.js` 2. Require the answer to survive deleting the last paragraph That property is the difference between help and spam. Files: `src/draft.js`, `tests/answer-first.test.js` 3. Store drafts, and offer copy rather than send There is no send. That is the point. Files: `src/views/match.ejs` ### Done when - [ ] A draft still answers with its last paragraph removed - [ ] A no-promotion subreddit yields a draft with no product - [ ] Rules are shown with their fetch time - [ ] No code path can post, vote or message ### Watch out - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ## Not in this build - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. ## After v1, if you want it - A second source of threads, such as a forum with an open API, added behind the same filters - A weekly digest of new matches by email, reusing the sweep output - Tracking which of your posted replies earned replies, entered by hand and shown against the drafts ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. DATABASE_PATH=./data/rankhog.db # Required · secret. From your registered application. Never in the repository. REDDIT_CLIENT_ID=redacted # Required · secret. Same application. Rotate it if it ever reaches a log. REDDIT_CLIENT_SECRET=redacted # Required. Names the tool, a version and a contact, as the platform's guidance asks. REDDIT_USER_AGENT=rankhog/0.1 by u-yourname (contact: you@example.com) # Required · secret. The ranking filter. Set a spend cap at the provider as well as here. SERP_API_KEY=redacted # Required. A hard stop. The sweep reports what it did not check. SERP_QUERIES_PER_SWEEP=25 # Required · secret. Drafting only. Its own key with its own limit. LLM_API_KEY=sk-...redacted # Required. Above this, promotional drafts are hidden and the reason is shown. PROMO_PER_SUB_PER_WEEK=1
You are building a lean indie version of Rankhog. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Rankhog · indie build A local opportunity finder whose hard problems are being a well-behaved API client without hard-coding a rate you cannot verify, spending a paid search budget only on threads worth checking, and drafting replies that answer a question rather than advertising. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node 22 command line sweep plus a review page on localhost | the sweep is a scheduled job, the review is a reading task | | Reddit access | Registered API credentials, a descriptive User-Agent, pacing taken from the response headers | unidentified traffic at a guessed rate is how a client gets blocked | | Storage | SQLite where a dismissed thread is dismissed permanently | the second time you see a thread you already rejected, you stop opening the tool | | Ranking filter | A search API behind a per-sweep query budget and a cache | every query costs money and the same keyword returns the same page all week | | Drafting | One model call per candidate, fed the subreddit's own rules, output stored and never sent | a draft is a starting point, not an outbox | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the sweep and the local review page. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Registered Reddit API credentials** · free for personal, non-commercial use; commercial use needs separate approval - Why: The API expects an identified client, and the terms you agree to when registering are the ones that govern what you may do with the data. - Get it: Create an application in your Reddit preferences, note the client identifier and secret, and read the current data API terms and developer guidance before writing the client. - Verify: An authenticated search returns results and the response carries rate limit headers you can read - [ ] **A search results API key** · a few pounds per thousand queries on most providers - Why: The ranking filter is the thing that separates a thread worth answering from one nobody will ever see, and it is the only per-query cost in the build. - Get it: Any search API with published per-query pricing and a spend cap you can set. - Verify: One keyword lookup returns results and the account shows the queries used - [ ] **An API key for a language model** · pennies per sweep at a few dozen drafts - Why: Drafting is the other external cost, and it should have its own limit rather than sharing one. - Get it: Create a key with its own spend limit. - Verify: A draft returns and the token usage is recorded - [ ] **A Reddit account with real history in your target subreddits** · free, and expensive in time - Why: This is the part the tool cannot give you, and the reason the paid product has a business. Without it the best draft in the world gets removed. - Get it: Comment usefully, without mentioning anything you sell, for several weeks. There is no shortcut and buying one is worse than nothing. - Verify: You have posted ordinary comments in each target subreddit over multiple weeks - [ ] **Your query budget, promotional cap and participation ratio** · free - Why: Each is enforced by code and asserted by a test, and choosing them after a bad week means choosing whatever already happened. - Get it: Write down the paid queries per sweep, the promotional replies per subreddit per week, and the ordinary to promotional ratio below which the tool stops suggesting promotion. - Verify: All three are in the configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. - account age, karma, and comment history in the subs that matter - the warm-up: weeks of ordinary participation before you can mention a product - posting through a real browser session rather than the API, which is what keeps accounts unflagged - per-subreddit rule knowledge and pacing judgment - shared accounts across a team, and the managed service If one of those is essential to you, that is the reason to keep paying for Rankhog, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Rankhog The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a Reddit opportunity finder that drafts a reply and leaves the posting to you to replace Rankhog. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this build stops where it does. Nothing here logs into Reddit, comments, votes, or sends a message. Not through a headless browser, not with a stored session cookie, not 'only on my own account'. Automated promotion is exactly what the platform's spam enforcement exists to catch, and the punishment is silent: a shadowbanned account's comments look live to the person who wrote them and are invisible to everybody else, so you carry on posting into nothing for weeks. This tool finds threads and drafts replies. You read the draft, you edit it, you post it yourself. Which means the first deliverable is not code. Spend a few weeks commenting usefully in the subreddits you care about before you mention anything you sell, and build this in the meantime. ### Stack (fixed, do not substitute) - Runtime: A Node 22 command line sweep plus a review page on localhost · the sweep is a scheduled job, the review is a reading task - Reddit access: Registered API credentials, a descriptive User-Agent, pacing taken from the response headers · unidentified traffic at a guessed rate is how a client gets blocked - Storage: SQLite where a dismissed thread is dismissed permanently · the second time you see a thread you already rejected, you stop opening the tool - Ranking filter: A search API behind a per-sweep query budget and a cache · every query costs money and the same keyword returns the same page all week - Drafting: One model call per candidate, fed the subreddit's own rules, output stored and never sent · a draft is a starting point, not an outbox ### Data model (create this before Phase 1) - keywords(id, text, subreddits_json, enabled, added_at) - threads(id, remote_id, subreddit, title, permalink, author, created_at, score, num_comments, fetched_at) - matches(id, thread_id, keyword_id, matched_at, state) · state is new, ranked, drafted, dismissed or posted_by_me - serp_cache(keyword_id, checked_at, results_json, queries_used) · one lookup per keyword per window, reused by every match - sub_rules(subreddit, rules_json, fetched_at, self_promo_summary) · pulled from the subreddit's own rules endpoint, refreshed on a schedule - drafts(match_id, model, prompt_version, text, created_at, edited_text, copied_at) - participation(subreddit, week, ordinary_comments, promotional_comments) · you fill this in, and the tool refuses to suggest more promotion when the ratio is wrong - Dismissal is permanent. A thread you rejected never appears again, because a finder that repeats itself is noise. ### Phase 1 · A client the platform can identify and slow down Build: Register an application and use its credentials. Send a User-Agent naming the tool, a version and a way to reach you. Read the rate limit headers on every response and sleep when the remaining allowance runs low. Treat a 429 as evidence your pacing is wrong, not as something to retry through. Run one sweep at a time. Trap: Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. Done when: No requests-per-minute constant exists anywhere in the code, verified by review; pacing comes from response headers. The User-Agent names the tool, a version and a contact, following the platform's current published guidance. A 429 halts the sweep, records the reset time and resumes afterwards rather than retrying immediately, tested with a fake server. Credentials live in the environment and never appear in a log line, tested. Do not build yet: No keyword sweep, ranking filter, drafting or interface. ### Phase 2 · The sweep, and the pile you already rejected Build: Search each enabled keyword across its subreddits, store threads with age, score and comment count, and record a match per keyword. Apply cheap filters first: thread age, minimum score, and whether the question has already been answered well in the top comments. Dismissal writes a permanent state and the sweep never selects it again. Trap: Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. Done when: A dismissed thread never appears in a later sweep, tested across two runs. Cheap filters run before anything paid, verified by counting the paid calls in a sweep with all candidates filtered out. Each keyword reports how many candidates it produced and how many survived, so a noisy keyword is visible. Re-running a sweep immediately produces no duplicate matches. Do not build yet: No ranking filter, drafting or costs yet. ### Phase 3 · Only the threads people actually land on Build: For each keyword that survived the filters, look up the search results once per window and cache them. Keep matches whose thread appears in those results. Count queries against a per-sweep budget and stop the sweep when it is reached, reporting what was left unchecked rather than quietly spending more. Trap: Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. Done when: A sweep with twenty matches across four keywords performs at most four paid lookups, asserted by counting calls. Reaching the budget stops the sweep and reports which keywords were unchecked, tested by setting the budget to one. The cache is honoured within its window and expires after it, tested with a clock. The cost of the last sweep is shown in the interface, not only in a log. Do not build yet: No drafting or pacing counters yet. ### Phase 4 · Drafts that answer the question Build: Fetch each subreddit's rules and summarise the self-promotion position. Draft with the thread text and those rules in the prompt, instructed to answer the question in the first paragraph and to mention the product last, briefly, and only where it genuinely fits. Where the rules prohibit promotion, the draft says so and mentions nothing. Store the draft; never send it. Trap: A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. Done when: A draft whose final paragraph is deleted still answers the question, checked as an explicit property before display. A subreddit whose rules prohibit self-promotion produces a draft mentioning no product and says which rule applied. Rules are fetched from the subreddit and shown beside the draft, with their fetch time. Nothing in the codebase can post, comment, vote or message, verified by review and stated in the README. Do not build yet: No pacing counters or export yet. ### Phase 5 · The pace that keeps the account alive Build: Record ordinary comments and promotional comments per subreddit per week, entered by you. Cap suggested promotional drafts per subreddit per week and hide them when the ratio of ordinary to promotional participation falls below your stated line. Show account age and week count as a warm-up progress view. Export matches, drafts and history as CSV and JSON. Trap: The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. Done when: Exceeding the weekly promotional cap for a subreddit hides further promotional drafts and states why, tested. The participation ratio is entered by you and a subreddit below the line shows only non-promotional drafts. The warm-up view shows account age and weeks of ordinary participation per subreddit. The export re-imports into an empty instance with matches, drafts and dismissals intact. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that posts, comments, votes, messages or logs in on your behalf, including a headless browser driving your own session. This is the line the whole build is organised around. - Buying, renting or running multiple accounts, which is against the platform's rules and is the fastest route to losing all of them. - Any promise that a thread will be read or cited by an AI answer engine. Nobody can commit to that, and a feature implying it is selling a hope. - Commercial or resale use of the platform's data, which needs separate written approval under the current data API terms; read them before you build anything beyond personal use. ### README must contain - That nothing in this tool posts, comments, votes or logs into Reddit, and why that line exists - That a shadowban is silent, so automated promotion fails invisibly for weeks - That pacing comes from the API's own rate limit headers and no rate constant exists in the code - The cost of one sweep, the query budget, and where the budget stops - That you should spend a few weeks commenting normally in your target subreddits before mentioning anything you sell ===== AGENTS.md ===== # Agent instructions · Rankhog indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Node 22 command line sweep plus a review page on localhost, Registered API credentials, a descriptive User-Agent, pacing taken from the response headers, SQLite where a dismissed thread is dismissed permanently, A search API behind a per-sweep query budget and a cache, One model call per candidate, fed the subreddit's own rules, output stored and never sent. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ===== BUILD_PLAN.md ===== # Build plan · Rankhog A local opportunity finder whose hard problems are being a well-behaved API client without hard-coding a rate you cannot verify, spending a paid search budget only on threads worth checking, and drafting replies that answer a question rather than advertising. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A client the platform can identify and slow down Every request is authenticated, identifiable, and paced by what the server says rather than by a number you typed. ### Steps 1. Authenticate with registered credentials Read the current terms before you write the client, not after it is blocked. Files: `src/reddit/client.js`, `.env.example` 2. Pace from the response headers The server already tells you the answer on every reply. Files: `src/reddit/pacing.js`, `tests/pacing.test.js` 3. Halt and resume on a rate limit response A retry loop through a 429 is the behaviour that earns a block. Files: `src/reddit/client.js` ### Done when - [ ] No rate constant exists in the code - [ ] The User-Agent names tool, version and contact - [ ] A 429 halts and resumes at the reset time - [ ] Credentials never reach a log ### Watch out - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. ## Phase 2 · The sweep, and the pile you already rejected A sweep turns keywords into candidate threads without ever resurfacing one you dismissed. ### Steps 1. Filter on age, score and answeredness first Everything free happens before anything billed. Files: `src/filters.js`, `tests/filters.test.js` 2. Make dismissal permanent and visible The dismissed pile is what makes the list readable on day ten. Files: `src/matches.js` 3. Report candidates per keyword A keyword that only produces noise should be deleted, not endured. Files: `src/sweep.js` ### Done when - [ ] Dismissed threads never return - [ ] Free filters run before paid calls - [ ] Each keyword reports its yield - [ ] A repeat sweep creates no duplicates ### Watch out - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. ## Phase 3 · Only the threads people actually land on A paid search lookup happens once per keyword per window and the budget is a hard stop. ### Steps 1. Look up per keyword, not per thread The ranking question belongs to the keyword. Files: `src/serp.js`, `tests/serp-cache.test.js` 2. Make the budget a hard stop A soft budget is a bill with an apology attached. Files: `src/budget.js` 3. Show the sweep's cost in the interface A cost you can see is a cost you can decide about. Files: `src/views/sweep.ejs` ### Done when - [ ] One paid lookup per keyword per window - [ ] The budget stops the sweep and reports the remainder - [ ] The cache expires with its window - [ ] The last sweep's cost is visible ### Watch out - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. ## Phase 4 · Drafts that answer the question Every draft opens with a real answer and respects the subreddit's own rules about promotion. ### Steps 1. Put the subreddit's rules in the prompt And show them beside the draft, with the fetch time. Files: `src/rules.js`, `src/draft.js` 2. Require the answer to survive deleting the last paragraph That property is the difference between help and spam. Files: `src/draft.js`, `tests/answer-first.test.js` 3. Store drafts, and offer copy rather than send There is no send. That is the point. Files: `src/views/match.ejs` ### Done when - [ ] A draft still answers with its last paragraph removed - [ ] A no-promotion subreddit yields a draft with no product - [ ] Rules are shown with their fetch time - [ ] No code path can post, vote or message ### Watch out - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ## Not in this build - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. ## After v1, if you want it - A second source of threads, such as a forum with an open API, added behind the same filters - A weekly digest of new matches by email, reusing the sweep output - Tracking which of your posted replies earned replies, entered by hand and shown against the drafts ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. DATABASE_PATH=./data/rankhog.db # Required · secret. From your registered application. Never in the repository. REDDIT_CLIENT_ID=redacted # Required · secret. Same application. Rotate it if it ever reaches a log. REDDIT_CLIENT_SECRET=redacted # Required. Names the tool, a version and a contact, as the platform's guidance asks. REDDIT_USER_AGENT=rankhog/0.1 by u-yourname (contact: you@example.com) # Required · secret. The ranking filter. Set a spend cap at the provider as well as here. SERP_API_KEY=redacted # Required. A hard stop. The sweep reports what it did not check. SERP_QUERIES_PER_SWEEP=25 # Required · secret. Drafting only. Its own key with its own limit. LLM_API_KEY=sk-...redacted # Required. Above this, promotional drafts are hidden and the reason is shown. PROMO_PER_SUB_PER_WEEK=1
You are building a production product version of Rankhog. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Rankhog · product brief ## Problem The code here is the easy half, and it is worth building. A script that watches subreddits, keeps the threads already ranking in Google, and drafts a reply against the sub's rules is a one-sitting build that will genuinely find you the conversations worth joining. The gap is what happens next. No agent writes you a Reddit account with a year of comment history in the subs you care about, and Reddit's spam enforcement is aimed exactly at accounts that show up new and start mentioning a product. A shadowban is silent: your comments look live to you and are invisible to everyone else, so you learn about it weeks later. Build the finder, then spend the weeks yourself, or pay someone to have already spent them. ## Product outcome A finder that surfaces the threads worth answering, drafts a reply that helps before it mentions anything, spends a budget you set, and refuses to suggest more promotion than your account has earned. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - LLM API key - SERP API key (Serper.dev) - a Reddit account you post from yourself - weeks of ordinary participation before promoting ## Explicit non-goals for v1 - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. - account age, karma, and comment history in the subs that matter - the warm-up: weeks of ordinary participation before you can mention a product - posting through a real browser session rather than the API, which is what keeps accounts unflagged - per-subreddit rule knowledge and pacing judgment - shared accounts across a team, and the managed service ## Success criteria - No rate constant is confirmed absent from the code - Dismissed threads are confirmed never to return - One paid lookup per keyword per window is confirmed - A draft is confirmed to answer with its last paragraph removed - The weekly promotional cap is confirmed to hide drafts ===== BRIEF.md ===== # Build brief · Rankhog The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a Reddit opportunity finder that drafts a reply and leaves the posting to you to replace Rankhog. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this build stops where it does. Nothing here logs into Reddit, comments, votes, or sends a message. Not through a headless browser, not with a stored session cookie, not 'only on my own account'. Automated promotion is exactly what the platform's spam enforcement exists to catch, and the punishment is silent: a shadowbanned account's comments look live to the person who wrote them and are invisible to everybody else, so you carry on posting into nothing for weeks. This tool finds threads and drafts replies. You read the draft, you edit it, you post it yourself. Which means the first deliverable is not code. Spend a few weeks commenting usefully in the subreddits you care about before you mention anything you sell, and build this in the meantime. ### Stack (fixed, do not substitute) - Runtime: A Node 22 command line sweep plus a review page on localhost · the sweep is a scheduled job, the review is a reading task - Reddit access: Registered API credentials, a descriptive User-Agent, pacing taken from the response headers · unidentified traffic at a guessed rate is how a client gets blocked - Storage: SQLite where a dismissed thread is dismissed permanently · the second time you see a thread you already rejected, you stop opening the tool - Ranking filter: A search API behind a per-sweep query budget and a cache · every query costs money and the same keyword returns the same page all week - Drafting: One model call per candidate, fed the subreddit's own rules, output stored and never sent · a draft is a starting point, not an outbox ### Data model (create this before Phase 1) - keywords(id, text, subreddits_json, enabled, added_at) - threads(id, remote_id, subreddit, title, permalink, author, created_at, score, num_comments, fetched_at) - matches(id, thread_id, keyword_id, matched_at, state) · state is new, ranked, drafted, dismissed or posted_by_me - serp_cache(keyword_id, checked_at, results_json, queries_used) · one lookup per keyword per window, reused by every match - sub_rules(subreddit, rules_json, fetched_at, self_promo_summary) · pulled from the subreddit's own rules endpoint, refreshed on a schedule - drafts(match_id, model, prompt_version, text, created_at, edited_text, copied_at) - participation(subreddit, week, ordinary_comments, promotional_comments) · you fill this in, and the tool refuses to suggest more promotion when the ratio is wrong - Dismissal is permanent. A thread you rejected never appears again, because a finder that repeats itself is noise. ### Phase 1 · A client the platform can identify and slow down Build: Register an application and use its credentials. Send a User-Agent naming the tool, a version and a way to reach you. Read the rate limit headers on every response and sleep when the remaining allowance runs low. Treat a 429 as evidence your pacing is wrong, not as something to retry through. Run one sweep at a time. Trap: Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. Done when: No requests-per-minute constant exists anywhere in the code, verified by review; pacing comes from response headers. The User-Agent names the tool, a version and a contact, following the platform's current published guidance. A 429 halts the sweep, records the reset time and resumes afterwards rather than retrying immediately, tested with a fake server. Credentials live in the environment and never appear in a log line, tested. Do not build yet: No keyword sweep, ranking filter, drafting or interface. ### Phase 2 · The sweep, and the pile you already rejected Build: Search each enabled keyword across its subreddits, store threads with age, score and comment count, and record a match per keyword. Apply cheap filters first: thread age, minimum score, and whether the question has already been answered well in the top comments. Dismissal writes a permanent state and the sweep never selects it again. Trap: Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. Done when: A dismissed thread never appears in a later sweep, tested across two runs. Cheap filters run before anything paid, verified by counting the paid calls in a sweep with all candidates filtered out. Each keyword reports how many candidates it produced and how many survived, so a noisy keyword is visible. Re-running a sweep immediately produces no duplicate matches. Do not build yet: No ranking filter, drafting or costs yet. ### Phase 3 · Only the threads people actually land on Build: For each keyword that survived the filters, look up the search results once per window and cache them. Keep matches whose thread appears in those results. Count queries against a per-sweep budget and stop the sweep when it is reached, reporting what was left unchecked rather than quietly spending more. Trap: Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. Done when: A sweep with twenty matches across four keywords performs at most four paid lookups, asserted by counting calls. Reaching the budget stops the sweep and reports which keywords were unchecked, tested by setting the budget to one. The cache is honoured within its window and expires after it, tested with a clock. The cost of the last sweep is shown in the interface, not only in a log. Do not build yet: No drafting or pacing counters yet. ### Phase 4 · Drafts that answer the question Build: Fetch each subreddit's rules and summarise the self-promotion position. Draft with the thread text and those rules in the prompt, instructed to answer the question in the first paragraph and to mention the product last, briefly, and only where it genuinely fits. Where the rules prohibit promotion, the draft says so and mentions nothing. Store the draft; never send it. Trap: A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. Done when: A draft whose final paragraph is deleted still answers the question, checked as an explicit property before display. A subreddit whose rules prohibit self-promotion produces a draft mentioning no product and says which rule applied. Rules are fetched from the subreddit and shown beside the draft, with their fetch time. Nothing in the codebase can post, comment, vote or message, verified by review and stated in the README. Do not build yet: No pacing counters or export yet. ### Phase 5 · The pace that keeps the account alive Build: Record ordinary comments and promotional comments per subreddit per week, entered by you. Cap suggested promotional drafts per subreddit per week and hide them when the ratio of ordinary to promotional participation falls below your stated line. Show account age and week count as a warm-up progress view. Export matches, drafts and history as CSV and JSON. Trap: The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. Done when: Exceeding the weekly promotional cap for a subreddit hides further promotional drafts and states why, tested. The participation ratio is entered by you and a subreddit below the line shows only non-promotional drafts. The warm-up view shows account age and weeks of ordinary participation per subreddit. The export re-imports into an empty instance with matches, drafts and dismissals intact. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that posts, comments, votes, messages or logs in on your behalf, including a headless browser driving your own session. This is the line the whole build is organised around. - Buying, renting or running multiple accounts, which is against the platform's rules and is the fastest route to losing all of them. - Any promise that a thread will be read or cited by an AI answer engine. Nobody can commit to that, and a feature implying it is selling a hope. - Commercial or resale use of the platform's data, which needs separate written approval under the current data API terms; read them before you build anything beyond personal use. ### README must contain - That nothing in this tool posts, comments, votes or logs into Reddit, and why that line exists - That a shadowban is silent, so automated promotion fails invisibly for weeks - That pacing comes from the API's own rate limit headers and no rate constant exists in the code - The cost of one sweep, the query budget, and where the budget stops - That you should spend a few weeks commenting normally in your target subreddits before mentioning anything you sell ===== ARCHITECTURE.md ===== # Architecture · Rankhog ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node 22 command line sweep plus a review page on localhost | the sweep is a scheduled job, the review is a reading task | | Reddit access | Registered API credentials, a descriptive User-Agent, pacing taken from the response headers | unidentified traffic at a guessed rate is how a client gets blocked | | Storage | SQLite where a dismissed thread is dismissed permanently | the second time you see a thread you already rejected, you stop opening the tool | | Ranking filter | A search API behind a per-sweep query budget and a cache | every query costs money and the same keyword returns the same page all week | | Drafting | One model call per candidate, fed the subreddit's own rules, output stored and never sent | a draft is a starting point, not an outbox | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Client | Authentication, identification and header-driven pacing. | Any transport, provided no rate constant appears in the code. | | Sweep | Keyword search, cheap filters and permanent dismissal. | Any filter set, provided free filters run before paid calls. | | Ranking | Per-keyword lookups, caching and the hard budget. | Any search provider with published per-query pricing. | | Drafting | Rules-aware drafts stored and never sent. | Any model, provided the answer survives deleting the last paragraph. | | Pacing | Promotional caps and the participation ratio. | Not swappable for nothing, which is how accounts get flagged. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. - `REDDIT_CLIENT_ID` · required, secret · From your registered application. Never in the repository. - `REDDIT_CLIENT_SECRET` · required, secret · Same application. Rotate it if it ever reaches a log. - `REDDIT_USER_AGENT` · required · Names the tool, a version and a contact, as the platform's guidance asks. - `SERP_API_KEY` · required, secret · The ranking filter. Set a spend cap at the provider as well as here. - `SERP_QUERIES_PER_SWEEP` · required · A hard stop. The sweep reports what it did not check. - `LLM_API_KEY` · required, secret · Drafting only. Its own key with its own limit. - `PROMO_PER_SUB_PER_WEEK` · required · Above this, promotional drafts are hidden and the reason is shown. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Rankhog product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Node 22 command line sweep plus a review page on localhost, Registered API credentials, a descriptive User-Agent, pacing taken from the response headers, SQLite where a dismissed thread is dismissed permanently, A search API behind a per-sweep query budget and a cache, One model call per candidate, fed the subreddit's own rules, output stored and never sent. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. - The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. ===== MILESTONES.md ===== # Delivery milestones · Rankhog Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A client the platform can identify and slow down Every request is authenticated, identifiable, and paced by what the server says rather than by a number you typed. ### Steps 1. Authenticate with registered credentials Read the current terms before you write the client, not after it is blocked. Files: `src/reddit/client.js`, `.env.example` 2. Pace from the response headers The server already tells you the answer on every reply. Files: `src/reddit/pacing.js`, `tests/pacing.test.js` 3. Halt and resume on a rate limit response A retry loop through a 429 is the behaviour that earns a block. Files: `src/reddit/client.js` ### Done when - [ ] No rate constant exists in the code - [ ] The User-Agent names tool, version and contact - [ ] A 429 halts and resumes at the reset time - [ ] Credentials never reach a log ### Watch out - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. ## M2 · The sweep, and the pile you already rejected A sweep turns keywords into candidate threads without ever resurfacing one you dismissed. ### Steps 1. Filter on age, score and answeredness first Everything free happens before anything billed. Files: `src/filters.js`, `tests/filters.test.js` 2. Make dismissal permanent and visible The dismissed pile is what makes the list readable on day ten. Files: `src/matches.js` 3. Report candidates per keyword A keyword that only produces noise should be deleted, not endured. Files: `src/sweep.js` ### Done when - [ ] Dismissed threads never return - [ ] Free filters run before paid calls - [ ] Each keyword reports its yield - [ ] A repeat sweep creates no duplicates ### Watch out - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. ## M3 · Only the threads people actually land on A paid search lookup happens once per keyword per window and the budget is a hard stop. ### Steps 1. Look up per keyword, not per thread The ranking question belongs to the keyword. Files: `src/serp.js`, `tests/serp-cache.test.js` 2. Make the budget a hard stop A soft budget is a bill with an apology attached. Files: `src/budget.js` 3. Show the sweep's cost in the interface A cost you can see is a cost you can decide about. Files: `src/views/sweep.ejs` ### Done when - [ ] One paid lookup per keyword per window - [ ] The budget stops the sweep and reports the remainder - [ ] The cache expires with its window - [ ] The last sweep's cost is visible ### Watch out - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. ## M4 · Drafts that answer the question Every draft opens with a real answer and respects the subreddit's own rules about promotion. ### Steps 1. Put the subreddit's rules in the prompt And show them beside the draft, with the fetch time. Files: `src/rules.js`, `src/draft.js` 2. Require the answer to survive deleting the last paragraph That property is the difference between help and spam. Files: `src/draft.js`, `tests/answer-first.test.js` 3. Store drafts, and offer copy rather than send There is no send. That is the point. Files: `src/views/match.ejs` ### Done when - [ ] A draft still answers with its last paragraph removed - [ ] A no-promotion subreddit yields a draft with no product - [ ] Rules are shown with their fetch time - [ ] No code path can post, vote or message ### Watch out - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ## M5 · The pace that keeps the account alive (production only) The tool refuses to suggest more promotion than the account has earned. ### Steps 1. Cap promotional suggestions per subreddit per week A visible cap you set beats a burst you regret. Files: `src/pacing.js`, `tests/promo-cap.test.js` 2. Track the ordinary to promotional ratio It is the number moderators are actually judging. Files: `src/participation.js` 3. Export everything as plain files Including dismissals, which are the part you cannot rebuild. Files: `src/export.js` ### Done when - [ ] The weekly promotional cap hides drafts and explains itself - [ ] A poor ratio yields only non-promotional drafts - [ ] The warm-up view shows age and weeks per subreddit - [ ] The export re-imports with dismissals intact ### Watch out - The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. ===== OPERATIONS.md ===== # Operations · Rankhog ## Backup The database, and particularly the dismissals and the participation log. Threads can be fetched again and drafts can be regenerated for a few pence; the record of what you rejected and how much you have promoted cannot. ## Restore Restore and run one sweep with the budget set to one, confirming that dismissed threads stay dismissed and that the promotional caps still apply from the restored participation log. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Queries used against the budget, drafts generated per week per subreddit, the ratio of ordinary to promotional participation, and the age of each subreddit's cached rules. A stale rules cache is how a draft breaks a rule that changed. ## Incident checklist If your comments stop getting replies or votes entirely, treat a shadowban as the first hypothesis and check by viewing your profile from a logged-out browser. The tool cannot detect this for you; that is why the pacing caps exist. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No rate constant is confirmed absent from the code - [ ] Dismissed threads are confirmed never to return - [ ] One paid lookup per keyword per window is confirmed - [ ] A draft is confirmed to answer with its last paragraph removed - [ ] The weekly promotional cap is confirmed to hide drafts ## Launch constraint Do not market omitted Rankhog capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. DATABASE_PATH=./data/rankhog.db # Required · secret. From your registered application. Never in the repository. REDDIT_CLIENT_ID=redacted # Required · secret. Same application. Rotate it if it ever reaches a log. REDDIT_CLIENT_SECRET=redacted # Required. Names the tool, a version and a contact, as the platform's guidance asks. REDDIT_USER_AGENT=rankhog/0.1 by u-yourname (contact: you@example.com) # Required · secret. The ranking filter. Set a spend cap at the provider as well as here. SERP_API_KEY=redacted # Required. A hard stop. The sweep reports what it did not check. SERP_QUERIES_PER_SWEEP=25 # Required · secret. Drafting only. Its own key with its own limit. LLM_API_KEY=sk-...redacted # Required. Above this, promotional drafts are hidden and the reason is shown. PROMO_PER_SUB_PER_WEEK=1
# Rankhog · indie build A local opportunity finder whose hard problems are being a well-behaved API client without hard-coding a rate you cannot verify, spending a paid search budget only on threads worth checking, and drafting replies that answer a question rather than advertising. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node 22 command line sweep plus a review page on localhost | the sweep is a scheduled job, the review is a reading task | | Reddit access | Registered API credentials, a descriptive User-Agent, pacing taken from the response headers | unidentified traffic at a guessed rate is how a client gets blocked | | Storage | SQLite where a dismissed thread is dismissed permanently | the second time you see a thread you already rejected, you stop opening the tool | | Ranking filter | A search API behind a per-sweep query budget and a cache | every query costs money and the same keyword returns the same page all week | | Drafting | One model call per candidate, fed the subreddit's own rules, output stored and never sent | a draft is a starting point, not an outbox | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the sweep and the local review page. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Registered Reddit API credentials** · free for personal, non-commercial use; commercial use needs separate approval - Why: The API expects an identified client, and the terms you agree to when registering are the ones that govern what you may do with the data. - Get it: Create an application in your Reddit preferences, note the client identifier and secret, and read the current data API terms and developer guidance before writing the client. - Verify: An authenticated search returns results and the response carries rate limit headers you can read - [ ] **A search results API key** · a few pounds per thousand queries on most providers - Why: The ranking filter is the thing that separates a thread worth answering from one nobody will ever see, and it is the only per-query cost in the build. - Get it: Any search API with published per-query pricing and a spend cap you can set. - Verify: One keyword lookup returns results and the account shows the queries used - [ ] **An API key for a language model** · pennies per sweep at a few dozen drafts - Why: Drafting is the other external cost, and it should have its own limit rather than sharing one. - Get it: Create a key with its own spend limit. - Verify: A draft returns and the token usage is recorded - [ ] **A Reddit account with real history in your target subreddits** · free, and expensive in time - Why: This is the part the tool cannot give you, and the reason the paid product has a business. Without it the best draft in the world gets removed. - Get it: Comment usefully, without mentioning anything you sell, for several weeks. There is no shortcut and buying one is worse than nothing. - Verify: You have posted ordinary comments in each target subreddit over multiple weeks - [ ] **Your query budget, promotional cap and participation ratio** · free - Why: Each is enforced by code and asserted by a test, and choosing them after a bad week means choosing whatever already happened. - Get it: Write down the paid queries per sweep, the promotional replies per subreddit per week, and the ordinary to promotional ratio below which the tool stops suggesting promotion. - Verify: All three are in the configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. - account age, karma, and comment history in the subs that matter - the warm-up: weeks of ordinary participation before you can mention a product - posting through a real browser session rather than the API, which is what keeps accounts unflagged - per-subreddit rule knowledge and pacing judgment - shared accounts across a team, and the managed service If one of those is essential to you, that is the reason to keep paying for Rankhog, and the README should say so rather than pretend.
# Build brief · Rankhog The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a Reddit opportunity finder that drafts a reply and leaves the posting to you to replace Rankhog. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this build stops where it does. Nothing here logs into Reddit, comments, votes, or sends a message. Not through a headless browser, not with a stored session cookie, not 'only on my own account'. Automated promotion is exactly what the platform's spam enforcement exists to catch, and the punishment is silent: a shadowbanned account's comments look live to the person who wrote them and are invisible to everybody else, so you carry on posting into nothing for weeks. This tool finds threads and drafts replies. You read the draft, you edit it, you post it yourself. Which means the first deliverable is not code. Spend a few weeks commenting usefully in the subreddits you care about before you mention anything you sell, and build this in the meantime. ### Stack (fixed, do not substitute) - Runtime: A Node 22 command line sweep plus a review page on localhost · the sweep is a scheduled job, the review is a reading task - Reddit access: Registered API credentials, a descriptive User-Agent, pacing taken from the response headers · unidentified traffic at a guessed rate is how a client gets blocked - Storage: SQLite where a dismissed thread is dismissed permanently · the second time you see a thread you already rejected, you stop opening the tool - Ranking filter: A search API behind a per-sweep query budget and a cache · every query costs money and the same keyword returns the same page all week - Drafting: One model call per candidate, fed the subreddit's own rules, output stored and never sent · a draft is a starting point, not an outbox ### Data model (create this before Phase 1) - keywords(id, text, subreddits_json, enabled, added_at) - threads(id, remote_id, subreddit, title, permalink, author, created_at, score, num_comments, fetched_at) - matches(id, thread_id, keyword_id, matched_at, state) · state is new, ranked, drafted, dismissed or posted_by_me - serp_cache(keyword_id, checked_at, results_json, queries_used) · one lookup per keyword per window, reused by every match - sub_rules(subreddit, rules_json, fetched_at, self_promo_summary) · pulled from the subreddit's own rules endpoint, refreshed on a schedule - drafts(match_id, model, prompt_version, text, created_at, edited_text, copied_at) - participation(subreddit, week, ordinary_comments, promotional_comments) · you fill this in, and the tool refuses to suggest more promotion when the ratio is wrong - Dismissal is permanent. A thread you rejected never appears again, because a finder that repeats itself is noise. ### Phase 1 · A client the platform can identify and slow down Build: Register an application and use its credentials. Send a User-Agent naming the tool, a version and a way to reach you. Read the rate limit headers on every response and sleep when the remaining allowance runs low. Treat a 429 as evidence your pacing is wrong, not as something to retry through. Run one sweep at a time. Trap: Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. Done when: No requests-per-minute constant exists anywhere in the code, verified by review; pacing comes from response headers. The User-Agent names the tool, a version and a contact, following the platform's current published guidance. A 429 halts the sweep, records the reset time and resumes afterwards rather than retrying immediately, tested with a fake server. Credentials live in the environment and never appear in a log line, tested. Do not build yet: No keyword sweep, ranking filter, drafting or interface. ### Phase 2 · The sweep, and the pile you already rejected Build: Search each enabled keyword across its subreddits, store threads with age, score and comment count, and record a match per keyword. Apply cheap filters first: thread age, minimum score, and whether the question has already been answered well in the top comments. Dismissal writes a permanent state and the sweep never selects it again. Trap: Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. Done when: A dismissed thread never appears in a later sweep, tested across two runs. Cheap filters run before anything paid, verified by counting the paid calls in a sweep with all candidates filtered out. Each keyword reports how many candidates it produced and how many survived, so a noisy keyword is visible. Re-running a sweep immediately produces no duplicate matches. Do not build yet: No ranking filter, drafting or costs yet. ### Phase 3 · Only the threads people actually land on Build: For each keyword that survived the filters, look up the search results once per window and cache them. Keep matches whose thread appears in those results. Count queries against a per-sweep budget and stop the sweep when it is reached, reporting what was left unchecked rather than quietly spending more. Trap: Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. Done when: A sweep with twenty matches across four keywords performs at most four paid lookups, asserted by counting calls. Reaching the budget stops the sweep and reports which keywords were unchecked, tested by setting the budget to one. The cache is honoured within its window and expires after it, tested with a clock. The cost of the last sweep is shown in the interface, not only in a log. Do not build yet: No drafting or pacing counters yet. ### Phase 4 · Drafts that answer the question Build: Fetch each subreddit's rules and summarise the self-promotion position. Draft with the thread text and those rules in the prompt, instructed to answer the question in the first paragraph and to mention the product last, briefly, and only where it genuinely fits. Where the rules prohibit promotion, the draft says so and mentions nothing. Store the draft; never send it. Trap: A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. Done when: A draft whose final paragraph is deleted still answers the question, checked as an explicit property before display. A subreddit whose rules prohibit self-promotion produces a draft mentioning no product and says which rule applied. Rules are fetched from the subreddit and shown beside the draft, with their fetch time. Nothing in the codebase can post, comment, vote or message, verified by review and stated in the README. Do not build yet: No pacing counters or export yet. ### Phase 5 · The pace that keeps the account alive Build: Record ordinary comments and promotional comments per subreddit per week, entered by you. Cap suggested promotional drafts per subreddit per week and hide them when the ratio of ordinary to promotional participation falls below your stated line. Show account age and week count as a warm-up progress view. Export matches, drafts and history as CSV and JSON. Trap: The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. Done when: Exceeding the weekly promotional cap for a subreddit hides further promotional drafts and states why, tested. The participation ratio is entered by you and a subreddit below the line shows only non-promotional drafts. The warm-up view shows account age and weeks of ordinary participation per subreddit. The export re-imports into an empty instance with matches, drafts and dismissals intact. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that posts, comments, votes, messages or logs in on your behalf, including a headless browser driving your own session. This is the line the whole build is organised around. - Buying, renting or running multiple accounts, which is against the platform's rules and is the fastest route to losing all of them. - Any promise that a thread will be read or cited by an AI answer engine. Nobody can commit to that, and a feature implying it is selling a hope. - Commercial or resale use of the platform's data, which needs separate written approval under the current data API terms; read them before you build anything beyond personal use. ### README must contain - That nothing in this tool posts, comments, votes or logs into Reddit, and why that line exists - That a shadowban is silent, so automated promotion fails invisibly for weeks - That pacing comes from the API's own rate limit headers and no rate constant exists in the code - The cost of one sweep, the query budget, and where the budget stops - That you should spend a few weeks commenting normally in your target subreddits before mentioning anything you sell
# Agent instructions · Rankhog indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Node 22 command line sweep plus a review page on localhost, Registered API credentials, a descriptive User-Agent, pacing taken from the response headers, SQLite where a dismissed thread is dismissed permanently, A search API behind a per-sweep query budget and a cache, One model call per candidate, fed the subreddit's own rules, output stored and never sent. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft.
# Build plan · Rankhog A local opportunity finder whose hard problems are being a well-behaved API client without hard-coding a rate you cannot verify, spending a paid search budget only on threads worth checking, and drafting replies that answer a question rather than advertising. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A client the platform can identify and slow down Every request is authenticated, identifiable, and paced by what the server says rather than by a number you typed. ### Steps 1. Authenticate with registered credentials Read the current terms before you write the client, not after it is blocked. Files: `src/reddit/client.js`, `.env.example` 2. Pace from the response headers The server already tells you the answer on every reply. Files: `src/reddit/pacing.js`, `tests/pacing.test.js` 3. Halt and resume on a rate limit response A retry loop through a 429 is the behaviour that earns a block. Files: `src/reddit/client.js` ### Done when - [ ] No rate constant exists in the code - [ ] The User-Agent names tool, version and contact - [ ] A 429 halts and resumes at the reset time - [ ] Credentials never reach a log ### Watch out - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. ## Phase 2 · The sweep, and the pile you already rejected A sweep turns keywords into candidate threads without ever resurfacing one you dismissed. ### Steps 1. Filter on age, score and answeredness first Everything free happens before anything billed. Files: `src/filters.js`, `tests/filters.test.js` 2. Make dismissal permanent and visible The dismissed pile is what makes the list readable on day ten. Files: `src/matches.js` 3. Report candidates per keyword A keyword that only produces noise should be deleted, not endured. Files: `src/sweep.js` ### Done when - [ ] Dismissed threads never return - [ ] Free filters run before paid calls - [ ] Each keyword reports its yield - [ ] A repeat sweep creates no duplicates ### Watch out - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. ## Phase 3 · Only the threads people actually land on A paid search lookup happens once per keyword per window and the budget is a hard stop. ### Steps 1. Look up per keyword, not per thread The ranking question belongs to the keyword. Files: `src/serp.js`, `tests/serp-cache.test.js` 2. Make the budget a hard stop A soft budget is a bill with an apology attached. Files: `src/budget.js` 3. Show the sweep's cost in the interface A cost you can see is a cost you can decide about. Files: `src/views/sweep.ejs` ### Done when - [ ] One paid lookup per keyword per window - [ ] The budget stops the sweep and reports the remainder - [ ] The cache expires with its window - [ ] The last sweep's cost is visible ### Watch out - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. ## Phase 4 · Drafts that answer the question Every draft opens with a real answer and respects the subreddit's own rules about promotion. ### Steps 1. Put the subreddit's rules in the prompt And show them beside the draft, with the fetch time. Files: `src/rules.js`, `src/draft.js` 2. Require the answer to survive deleting the last paragraph That property is the difference between help and spam. Files: `src/draft.js`, `tests/answer-first.test.js` 3. Store drafts, and offer copy rather than send There is no send. That is the point. Files: `src/views/match.ejs` ### Done when - [ ] A draft still answers with its last paragraph removed - [ ] A no-promotion subreddit yields a draft with no product - [ ] Rules are shown with their fetch time - [ ] No code path can post, vote or message ### Watch out - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ## Not in this build - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. ## After v1, if you want it - A second source of threads, such as a forum with an open API, added behind the same filters - A weekly digest of new matches by email, reusing the sweep output - Tracking which of your posted replies earned replies, entered by hand and shown against the drafts
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. DATABASE_PATH=./data/rankhog.db # Required · secret. From your registered application. Never in the repository. REDDIT_CLIENT_ID=redacted # Required · secret. Same application. Rotate it if it ever reaches a log. REDDIT_CLIENT_SECRET=redacted # Required. Names the tool, a version and a contact, as the platform's guidance asks. REDDIT_USER_AGENT=rankhog/0.1 by u-yourname (contact: you@example.com) # Required · secret. The ranking filter. Set a spend cap at the provider as well as here. SERP_API_KEY=redacted # Required. A hard stop. The sweep reports what it did not check. SERP_QUERIES_PER_SWEEP=25 # Required · secret. Drafting only. Its own key with its own limit. LLM_API_KEY=sk-...redacted # Required. Above this, promotional drafts are hidden and the reason is shown. PROMO_PER_SUB_PER_WEEK=1
# Rankhog · product brief ## Problem The code here is the easy half, and it is worth building. A script that watches subreddits, keeps the threads already ranking in Google, and drafts a reply against the sub's rules is a one-sitting build that will genuinely find you the conversations worth joining. The gap is what happens next. No agent writes you a Reddit account with a year of comment history in the subs you care about, and Reddit's spam enforcement is aimed exactly at accounts that show up new and start mentioning a product. A shadowban is silent: your comments look live to you and are invisible to everyone else, so you learn about it weeks later. Build the finder, then spend the weeks yourself, or pay someone to have already spent them. ## Product outcome A finder that surfaces the threads worth answering, drafts a reply that helps before it mentions anything, spends a budget you set, and refuses to suggest more promotion than your account has earned. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - LLM API key - SERP API key (Serper.dev) - a Reddit account you post from yourself - weeks of ordinary participation before promoting ## Explicit non-goals for v1 - Any posting, commenting, voting or login. - Multiple or purchased accounts. - Promises about AI answer citations. - Commercial use of the platform's data without approval. - account age, karma, and comment history in the subs that matter - the warm-up: weeks of ordinary participation before you can mention a product - posting through a real browser session rather than the API, which is what keeps accounts unflagged - per-subreddit rule knowledge and pacing judgment - shared accounts across a team, and the managed service ## Success criteria - No rate constant is confirmed absent from the code - Dismissed threads are confirmed never to return - One paid lookup per keyword per window is confirmed - A draft is confirmed to answer with its last paragraph removed - The weekly promotional cap is confirmed to hide drafts
# Build brief · Rankhog The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a Reddit opportunity finder that drafts a reply and leaves the posting to you to replace Rankhog. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this build stops where it does. Nothing here logs into Reddit, comments, votes, or sends a message. Not through a headless browser, not with a stored session cookie, not 'only on my own account'. Automated promotion is exactly what the platform's spam enforcement exists to catch, and the punishment is silent: a shadowbanned account's comments look live to the person who wrote them and are invisible to everybody else, so you carry on posting into nothing for weeks. This tool finds threads and drafts replies. You read the draft, you edit it, you post it yourself. Which means the first deliverable is not code. Spend a few weeks commenting usefully in the subreddits you care about before you mention anything you sell, and build this in the meantime. ### Stack (fixed, do not substitute) - Runtime: A Node 22 command line sweep plus a review page on localhost · the sweep is a scheduled job, the review is a reading task - Reddit access: Registered API credentials, a descriptive User-Agent, pacing taken from the response headers · unidentified traffic at a guessed rate is how a client gets blocked - Storage: SQLite where a dismissed thread is dismissed permanently · the second time you see a thread you already rejected, you stop opening the tool - Ranking filter: A search API behind a per-sweep query budget and a cache · every query costs money and the same keyword returns the same page all week - Drafting: One model call per candidate, fed the subreddit's own rules, output stored and never sent · a draft is a starting point, not an outbox ### Data model (create this before Phase 1) - keywords(id, text, subreddits_json, enabled, added_at) - threads(id, remote_id, subreddit, title, permalink, author, created_at, score, num_comments, fetched_at) - matches(id, thread_id, keyword_id, matched_at, state) · state is new, ranked, drafted, dismissed or posted_by_me - serp_cache(keyword_id, checked_at, results_json, queries_used) · one lookup per keyword per window, reused by every match - sub_rules(subreddit, rules_json, fetched_at, self_promo_summary) · pulled from the subreddit's own rules endpoint, refreshed on a schedule - drafts(match_id, model, prompt_version, text, created_at, edited_text, copied_at) - participation(subreddit, week, ordinary_comments, promotional_comments) · you fill this in, and the tool refuses to suggest more promotion when the ratio is wrong - Dismissal is permanent. A thread you rejected never appears again, because a finder that repeats itself is noise. ### Phase 1 · A client the platform can identify and slow down Build: Register an application and use its credentials. Send a User-Agent naming the tool, a version and a way to reach you. Read the rate limit headers on every response and sleep when the remaining allowance runs low. Treat a 429 as evidence your pacing is wrong, not as something to retry through. Run one sweep at a time. Trap: Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. Done when: No requests-per-minute constant exists anywhere in the code, verified by review; pacing comes from response headers. The User-Agent names the tool, a version and a contact, following the platform's current published guidance. A 429 halts the sweep, records the reset time and resumes afterwards rather than retrying immediately, tested with a fake server. Credentials live in the environment and never appear in a log line, tested. Do not build yet: No keyword sweep, ranking filter, drafting or interface. ### Phase 2 · The sweep, and the pile you already rejected Build: Search each enabled keyword across its subreddits, store threads with age, score and comment count, and record a match per keyword. Apply cheap filters first: thread age, minimum score, and whether the question has already been answered well in the top comments. Dismissal writes a permanent state and the sweep never selects it again. Trap: Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. Done when: A dismissed thread never appears in a later sweep, tested across two runs. Cheap filters run before anything paid, verified by counting the paid calls in a sweep with all candidates filtered out. Each keyword reports how many candidates it produced and how many survived, so a noisy keyword is visible. Re-running a sweep immediately produces no duplicate matches. Do not build yet: No ranking filter, drafting or costs yet. ### Phase 3 · Only the threads people actually land on Build: For each keyword that survived the filters, look up the search results once per window and cache them. Keep matches whose thread appears in those results. Count queries against a per-sweep budget and stop the sweep when it is reached, reporting what was left unchecked rather than quietly spending more. Trap: Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. Done when: A sweep with twenty matches across four keywords performs at most four paid lookups, asserted by counting calls. Reaching the budget stops the sweep and reports which keywords were unchecked, tested by setting the budget to one. The cache is honoured within its window and expires after it, tested with a clock. The cost of the last sweep is shown in the interface, not only in a log. Do not build yet: No drafting or pacing counters yet. ### Phase 4 · Drafts that answer the question Build: Fetch each subreddit's rules and summarise the self-promotion position. Draft with the thread text and those rules in the prompt, instructed to answer the question in the first paragraph and to mention the product last, briefly, and only where it genuinely fits. Where the rules prohibit promotion, the draft says so and mentions nothing. Store the draft; never send it. Trap: A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. Done when: A draft whose final paragraph is deleted still answers the question, checked as an explicit property before display. A subreddit whose rules prohibit self-promotion produces a draft mentioning no product and says which rule applied. Rules are fetched from the subreddit and shown beside the draft, with their fetch time. Nothing in the codebase can post, comment, vote or message, verified by review and stated in the README. Do not build yet: No pacing counters or export yet. ### Phase 5 · The pace that keeps the account alive Build: Record ordinary comments and promotional comments per subreddit per week, entered by you. Cap suggested promotional drafts per subreddit per week and hide them when the ratio of ordinary to promotional participation falls below your stated line. Show account age and week count as a warm-up progress view. Export matches, drafts and history as CSV and JSON. Trap: The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory. Done when: Exceeding the weekly promotional cap for a subreddit hides further promotional drafts and states why, tested. The participation ratio is entered by you and a subreddit below the line shows only non-promotional drafts. The warm-up view shows account age and weeks of ordinary participation per subreddit. The export re-imports into an empty instance with matches, drafts and dismissals intact. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that posts, comments, votes, messages or logs in on your behalf, including a headless browser driving your own session. This is the line the whole build is organised around. - Buying, renting or running multiple accounts, which is against the platform's rules and is the fastest route to losing all of them. - Any promise that a thread will be read or cited by an AI answer engine. Nobody can commit to that, and a feature implying it is selling a hope. - Commercial or resale use of the platform's data, which needs separate written approval under the current data API terms; read them before you build anything beyond personal use. ### README must contain - That nothing in this tool posts, comments, votes or logs into Reddit, and why that line exists - That a shadowban is silent, so automated promotion fails invisibly for weeks - That pacing comes from the API's own rate limit headers and no rate constant exists in the code - The cost of one sweep, the query budget, and where the budget stops - That you should spend a few weeks commenting normally in your target subreddits before mentioning anything you sell
# Architecture · Rankhog ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node 22 command line sweep plus a review page on localhost | the sweep is a scheduled job, the review is a reading task | | Reddit access | Registered API credentials, a descriptive User-Agent, pacing taken from the response headers | unidentified traffic at a guessed rate is how a client gets blocked | | Storage | SQLite where a dismissed thread is dismissed permanently | the second time you see a thread you already rejected, you stop opening the tool | | Ranking filter | A search API behind a per-sweep query budget and a cache | every query costs money and the same keyword returns the same page all week | | Drafting | One model call per candidate, fed the subreddit's own rules, output stored and never sent | a draft is a starting point, not an outbox | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Client | Authentication, identification and header-driven pacing. | Any transport, provided no rate constant appears in the code. | | Sweep | Keyword search, cheap filters and permanent dismissal. | Any filter set, provided free filters run before paid calls. | | Ranking | Per-keyword lookups, caching and the hard budget. | Any search provider with published per-query pricing. | | Drafting | Rules-aware drafts stored and never sent. | Any model, provided the answer survives deleting the last paragraph. | | Pacing | Promotional caps and the participation ratio. | Not swappable for nothing, which is how accounts get flagged. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. - `REDDIT_CLIENT_ID` · required, secret · From your registered application. Never in the repository. - `REDDIT_CLIENT_SECRET` · required, secret · Same application. Rotate it if it ever reaches a log. - `REDDIT_USER_AGENT` · required · Names the tool, a version and a contact, as the platform's guidance asks. - `SERP_API_KEY` · required, secret · The ranking filter. Set a spend cap at the provider as well as here. - `SERP_QUERIES_PER_SWEEP` · required · A hard stop. The sweep reports what it did not check. - `LLM_API_KEY` · required, secret · Drafting only. Its own key with its own limit. - `PROMO_PER_SUB_PER_WEEK` · required · Above this, promotional drafts are hidden and the reason is shown. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Rankhog product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Node 22 command line sweep plus a review page on localhost, Registered API credentials, a descriptive User-Agent, pacing taken from the response headers, SQLite where a dismissed thread is dismissed permanently, A search API behind a per-sweep query budget and a cache, One model call per candidate, fed the subreddit's own rules, output stored and never sent. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. - The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory.
# Delivery milestones · Rankhog Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A client the platform can identify and slow down Every request is authenticated, identifiable, and paced by what the server says rather than by a number you typed. ### Steps 1. Authenticate with registered credentials Read the current terms before you write the client, not after it is blocked. Files: `src/reddit/client.js`, `.env.example` 2. Pace from the response headers The server already tells you the answer on every reply. Files: `src/reddit/pacing.js`, `tests/pacing.test.js` 3. Halt and resume on a rate limit response A retry loop through a 429 is the behaviour that earns a block. Files: `src/reddit/client.js` ### Done when - [ ] No rate constant exists in the code - [ ] The User-Agent names tool, version and contact - [ ] A 429 halts and resumes at the reset time - [ ] Credentials never reach a log ### Watch out - Copying a requests-per-minute figure out of a blog post is wrong twice: the figure is stale, and it ignores that the server reports your remaining allowance and reset time on every single response. Pace from those headers, and keep no rate constant in the code at all so there is nothing to go out of date. ## M2 · The sweep, and the pile you already rejected A sweep turns keywords into candidate threads without ever resurfacing one you dismissed. ### Steps 1. Filter on age, score and answeredness first Everything free happens before anything billed. Files: `src/filters.js`, `tests/filters.test.js` 2. Make dismissal permanent and visible The dismissed pile is what makes the list readable on day ten. Files: `src/matches.js` 3. Report candidates per keyword A keyword that only produces noise should be deleted, not endured. Files: `src/sweep.js` ### Done when - [ ] Dismissed threads never return - [ ] Free filters run before paid calls - [ ] Each keyword reports its yield - [ ] A repeat sweep creates no duplicates ### Watch out - Storing matches without a dismissal state means every sweep hands you the same forty threads you looked at yesterday, plus two new ones, and you stop reading the list by the fourth day. Make dismissal permanent, show new matches first, and count how many candidates each keyword produced so a keyword that only generates noise is obvious. ## M3 · Only the threads people actually land on A paid search lookup happens once per keyword per window and the budget is a hard stop. ### Steps 1. Look up per keyword, not per thread The ranking question belongs to the keyword. Files: `src/serp.js`, `tests/serp-cache.test.js` 2. Make the budget a hard stop A soft budget is a bill with an apology attached. Files: `src/budget.js` 3. Show the sweep's cost in the interface A cost you can see is a cost you can decide about. Files: `src/views/sweep.ejs` ### Done when - [ ] One paid lookup per keyword per window - [ ] The budget stops the sweep and reports the remainder - [ ] The cache expires with its window - [ ] The last sweep's cost is visible ### Watch out - Checking every matched thread against the search API spends the budget on threads nobody ever sees, because the ranking question is about the keyword, not the thread: one lookup per keyword answers it for every match at once. Cache per keyword per window, and make the budget a hard stop that reports the remainder instead of a soft target that gets exceeded. ## M4 · Drafts that answer the question Every draft opens with a real answer and respects the subreddit's own rules about promotion. ### Steps 1. Put the subreddit's rules in the prompt And show them beside the draft, with the fetch time. Files: `src/rules.js`, `src/draft.js` 2. Require the answer to survive deleting the last paragraph That property is the difference between help and spam. Files: `src/draft.js`, `tests/answer-first.test.js` 3. Store drafts, and offer copy rather than send There is no send. That is the point. Files: `src/views/match.ejs` ### Done when - [ ] A draft still answers with its last paragraph removed - [ ] A no-promotion subreddit yields a draft with no product - [ ] Rules are shown with their fetch time - [ ] No code path can post, vote or message ### Watch out - A model told to write a reply promoting your product writes an advertisement with a question mark on the end, and that is precisely the comment that gets removed, reported, and remembered. Put the subreddit's own rules in the prompt, require the answer to stand on its own if the last sentence were deleted, and check that property before showing the draft. ## M5 · The pace that keeps the account alive (production only) The tool refuses to suggest more promotion than the account has earned. ### Steps 1. Cap promotional suggestions per subreddit per week A visible cap you set beats a burst you regret. Files: `src/pacing.js`, `tests/promo-cap.test.js` 2. Track the ordinary to promotional ratio It is the number moderators are actually judging. Files: `src/participation.js` 3. Export everything as plain files Including dismissals, which are the part you cannot rebuild. Files: `src/export.js` ### Done when - [ ] The weekly promotional cap hides drafts and explains itself - [ ] A poor ratio yields only non-promotional drafts - [ ] The warm-up view shows age and weeks per subreddit - [ ] The export re-imports with dismissals intact ### Watch out - The finder turns up forty good threads in its first week, you reply to all forty from an account three weeks old, and that burst is the exact pattern enforcement is tuned for; the account goes quiet and you cannot tell, because a shadowban looks normal from the inside. Cap promotional suggestions per subreddit per week and make the cap visible rather than advisory.
# Operations · Rankhog ## Backup The database, and particularly the dismissals and the participation log. Threads can be fetched again and drafts can be regenerated for a few pence; the record of what you rejected and how much you have promoted cannot. ## Restore Restore and run one sweep with the budget set to one, confirming that dismissed threads stay dismissed and that the promotional caps still apply from the restored participation log. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Queries used against the budget, drafts generated per week per subreddit, the ratio of ordinary to promotional participation, and the age of each subreddit's cached rules. A stale rules cache is how a draft breaks a rule that changed. ## Incident checklist If your comments stop getting replies or votes entirely, treat a shadowban as the first hypothesis and check by viewing your profile from a logged-out browser. The tool cannot detect this for you; that is why the pacing caps exist. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No rate constant is confirmed absent from the code - [ ] Dismissed threads are confirmed never to return - [ ] One paid lookup per keyword per window is confirmed - [ ] A draft is confirmed to answer with its last paragraph removed - [ ] The weekly promotional cap is confirmed to hide drafts ## Launch constraint Do not market omitted Rankhog capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Threads, matches, drafts and dismissals. The dismissals are the part you cannot rebuild. DATABASE_PATH=./data/rankhog.db # Required · secret. From your registered application. Never in the repository. REDDIT_CLIENT_ID=redacted # Required · secret. Same application. Rotate it if it ever reaches a log. REDDIT_CLIENT_SECRET=redacted # Required. Names the tool, a version and a contact, as the platform's guidance asks. REDDIT_USER_AGENT=rankhog/0.1 by u-yourname (contact: you@example.com) # Required · secret. The ranking filter. Set a spend cap at the provider as well as here. SERP_API_KEY=redacted # Required. A hard stop. The sweep reports what it did not check. SERP_QUERIES_PER_SWEEP=25 # Required · secret. Drafting only. Its own key with its own limit. LLM_API_KEY=sk-...redacted # Required. Above this, promotional drafts are hidden and the reason is shown. PROMO_PER_SUB_PER_WEEK=1
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because the failure mode is invisible and expensive. Getting a product mentioned on Reddit needs an account people and moderators already trust, and building one is weeks of participation before the first mention. People pay to skip the warm-up, to have posting happen through a real browser session instead of an API that gets flagged, and to have someone else carry the ban risk.
xaccount age, karma, and comment history in the subs that matter
xthe warm-up: weeks of ordinary participation before you can mention a product
xposting through a real browser session rather than the API, which is what keeps accounts unflagged
xper-subreddit rule knowledge and pacing judgment
xshared accounts across a team, and the managed service
Rankhog pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| standard | $99/workspace | — | 1 product; monitoring, strategy, and warm-up; 1 Reddit account warm-up; unlimited teammates. |
| managed reddit growth | $1000/workspace | — | Managed service starting at $1,000/month; exact posting, outreach, and reporting deliverables are custom. |
free tierno free tier (3-day card-required trial only)
billingmonthly only; no public annual plan
hidden costsEach additional product requires another $99/month Standard subscription.
verified 2026-08-14 · source ↗
Vibecode Rankhog
Kinda. The core of Rankhog is buildable in a weekend with the prompt on this page, but there are real gaps: account age, karma, and comment history in the subs that matter, the warm-up: weeks of ordinary participation before you can mention a product. Read the honest list above before committing.
How much does Rankhog cost?
Rankhog costs about $99/month (Standard, checked 2026-07-30), which is $1188 per year.
What do I lose by replacing Rankhog?
Honestly: account age, karma, and comment history in the subs that matter; the warm-up: weeks of ordinary participation before you can mention a product; posting through a real browser session rather than the API, which is what keeps accounts unflagged; per-subreddit rule knowledge and pacing judgment; shared accounts across a team, and the managed service. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Rankhog?
Yes: redsignal (Watches subreddits for keyword matches, filters the noise with an LLM, and drafts replies. Covers the finder half, no license declared.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.