Vibecode Bazzly
track this build4 phases, 12 steps, beginner friendly0%The listening half of this is not hard: Reddit exposes search and subreddit feeds, and an agent can rank hits for relevance and draft a comment in your voice by lunchtime. The half you cannot build is the account layer, meaning aged handles with real karma, subreddit-specific posting history, and enough trust to not get shadowbanned on the first link drop. If you are promoting one product from one account you actually own and post from, the DIY version covers most of the real value. If the reason you were paying is account inventory and rotation, no amount of code fixes that, because the scarce thing is time served on someone else's platform. Also worth saying out loud: the moment you are automating replies from accounts that are not yours, you are doing the thing Reddit bans people for, and no tool makes that safe.
You are building a lean indie version of Bazzly. 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 ===== # Bazzly · indie build A Reddit listener that finds threads worth replying to and drafts the reply, whose hard problems are staying inside the documented API and its published limits, keeping model spend below the subscription it replaces, and drafting something that does not invent a feature. 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 | One process with a scheduled poll on a machine that stays awake | a thread's useful window is its first few hours and a laptop lid closes | | API access | Reddit's OAuth data API through a registered script application | the documented route, with a credential you can revoke in one click | | Storage | SQLite, one row per thing the API returned, keyed on its fullname | dedup on the platform's own identifier, never on a title somebody can edit | | Scoring | A local keyword prefilter, then one model call per survivor | scoring every post in a busy subreddit costs more per month than the product | | Review | A server-rendered queue on localhost with copy, dismiss and mark-posted | the output is a draft in your clipboard, not an action on the network | | Spend | A daily cost ceiling checked by the scoring loop, not a warning in a log | the bill is the failure nobody notices until it arrives | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the poller, the scorer and the review page in one process. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **A Reddit account you actually use, with a registered script application** · free - Why: The only account this build ever touches, and the one a person will post from by hand. - Get it: Create a script-type application in your account's app preferences and note the client id, secret and redirect. - Verify: A token request succeeds and an authenticated listing returns with rate limit headers - [ ] **An API key for a text model** · a few pounds a month at the ceiling suggested here - Why: Relevance scoring and drafting both go through it, and it is the only recurring cost in the build. - Get it: Any provider whose pricing you can read per token. - Verify: One call returns valid JSON and its cost is recorded - [ ] **A one page fact sheet for your product** · free - Why: It is the only source a draft may make a claim from, so without it the drafting step is disabled rather than creative. - Get it: Write what the product does, what it costs, and three things it does not do. - Verify: Every claim in a generated draft maps to a line in it - [ ] **A machine that stays awake** (optional) · a few pounds a month, or free if you own the box - Why: A thread is worth answering in its first hours, and a laptop that sleeps turns the listener into a weekly digest. - Get it: A small always-on box, a virtual machine, or a home server. - Verify: A poll scheduled for four in the morning has rows by breakfast - [ ] **Your subreddit policies and your daily spend ceiling** · free - Why: Both are asserted by tests, and deciding them after seeing a bill or a ban means choosing whatever already happened. - Get it: Write the promotion policy for each subreddit you watch, with the date you read its rules, and pick a ceiling in pennies per day. - Verify: Both appear in configuration and are named in the 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: - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. - Aged, high-karma accounts with credible posting history - Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way - Their curated map of which subreddits tolerate product mentions and which will nuke you - A hosted dashboard, team seats, and shared approval queues - Someone else's problem when the API terms or rate limits change If one of those is essential to you, that is the reason to keep paying for Bazzly, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bazzly 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 listener that finds threads worth answering and drafts the reply for you to post yourself to replace Bazzly. 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 Nothing in this build posts to Reddit. Not a comment, not a vote, not a message. It finds threads, scores them and drafts a reply into a box you edit, and a human presses post from an account that human owns and reads. The reason is not squeamishness. The paid products in this category sell aged handles with karma already on them, and an automated reply from an account nobody lives in is the behaviour Reddit removes accounts for, so the feature you would be building is a way to lose the account you are promoting from. The second rule follows from the first. One account, yours, reached through a registered application with credentials you can revoke, using the documented API with a User-Agent that says who you are. No browser automation of a logged-in session, no rotation, no proxies. If what you were buying was account inventory, this build does not replace it and no code does, because the scarce thing is six years of somebody being a person on a website. ### Stack (fixed, do not substitute) - Runtime: One process with a scheduled poll on a machine that stays awake · a thread's useful window is its first few hours and a laptop lid closes - API access: Reddit's OAuth data API through a registered script application · the documented route, with a credential you can revoke in one click - Storage: SQLite, one row per thing the API returned, keyed on its fullname · dedup on the platform's own identifier, never on a title somebody can edit - Scoring: A local keyword prefilter, then one model call per survivor · scoring every post in a busy subreddit costs more per month than the product - Review: A server-rendered queue on localhost with copy, dismiss and mark-posted · the output is a draft in your clipboard, not an action on the network - Spend: A daily cost ceiling checked by the scoring loop, not a warning in a log · the bill is the failure nobody notices until it arrives ### Data model (create this before Phase 1) - subreddits(name, rules_fetched_at, promo_policy, allows_links, reviewed_by, reviewed_at) · what each community permits, recorded by a person - seen(fullname, kind, subreddit, created_utc, first_seen_at) · the dedup table, keyed on Reddit's own identifier - candidates(fullname, permalink, title, body_excerpt, author, subreddit, matched_terms_json, prefilter_score) - scores(fullname, model, relevance, asking_for_recommendation, reason, draft_reply, tokens_in, tokens_out, cost_cents, at) - decisions(fullname, action, acted_at, note) · queued, dismissed, irrelevant, posted_by_hand - api_calls(at, endpoint, status, ratelimit_remaining, ratelimit_reset, retry_after) · the evidence when you get throttled - facts(id, claim, source) · what your product actually does, and the only claims a draft is allowed to make - There is no posts table and no send function, because this build never writes anything to Reddit. ### Phase 1 · Reading Reddit the documented way Build: Register a script application, obtain a refresh token, and set a User-Agent naming the tool and your account. Poll each watched subreddit's new listing and each search term on a schedule. Record every call with the rate limit headers the response carried, back off on a 429 for the interval the response asks for, and read the currently published rate limit before choosing a poll interval rather than copying a number out of a blog post. Trap: Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. Done when: Running the poll twice in succession produces no duplicate candidates, tested back to back against a fixture. Every call is recorded with its status and the rate limit headers it returned. A 429 waits for the interval the response specifies rather than a fixed sleep, tested with a stubbed response. The User-Agent identifies the tool and the account, and a run with no credentials fails with a readable message instead of an anonymous request. Do not build yet: No scoring, no drafting, no review queue. ### Phase 2 · Two stages, because the model is the bill Build: Filter locally first on required terms, excluded terms, minimum body length, post age and score. Send only survivors to one structured model call returning strict JSON: a relevance figure, whether the author is actually asking for a recommendation, a one line reason and a draft. Price every call as you make it and stop scoring for the day when the ceiling is hit. Trap: Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. Done when: A run over a fixture of two hundred posts sends fewer than a stated fraction to the model and the fraction is asserted. Every score row carries its token counts and its computed cost. Reaching the daily ceiling stops scoring and says so on the queue, tested by setting the ceiling to zero. A malformed model response is rejected and retried once, leaving the candidate unscored rather than scored as zero. Do not build yet: No drafting rules, no community policies, no review interface. ### Phase 3 · Drafts that cannot invent a feature Build: Keep a short fact sheet: what the product does, what it costs, what it does not do. Give the model the thread, the fact sheet and a sample of your own writing. Run a second pass marking any sentence whose claim is not traceable to a fact, and show those highlighted in the queue. Require every draft to say plainly that you are the person who built the thing. Trap: A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. Done when: A draft referencing a capability absent from the fact sheet is flagged, tested with a sheet that omits a feature the thread asks about. Every draft contains the disclosure that you built the product, asserted in the suite. Drafts stay inside a stated word count. Removing the fact sheet stops drafting with a readable reason rather than drafting from nothing. Do not build yet: No community policies and no review interface yet. ### Phase 4 · Community rules as data Build: Record a policy per subreddit: promotion allowed, replies only when asked, or never. Fetch each subreddit's rules through the API and store the text beside your judgement with the date. Show the rules next to the draft. Default an unreviewed subreddit to never, and make reviewing it an explicit action with a name and a date attached. Trap: Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. Done when: An unreviewed subreddit produces candidates but no drafts, tested. Rules text is fetched, stored with its fetch date and rendered beside the draft. A subreddit marked never produces no draft at all under any relevance score. Changing a policy takes effect on the next run without editing code. Do not build yet: No review interface, export or demo mode yet. ### Phase 5 · A queue you work, and a key you can kill Build: A review queue sorted by relevance showing thread, reason, policy and an editable draft, with copy, dismiss and mark-posted all recorded. Pause everything on the first authentication failure with one alert instead of failing every poll. Export the history as CSV. A demo mode runs on fixture data with no credentials. Bind to localhost and keep every secret in the environment. Trap: A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. Done when: The queue shows thread, score, reason, subreddit policy and an editable draft for every candidate. Marking a thread posted records when and never shows it again. Revoking the token pauses the listener within one poll and states the reason on the page, tested by revoking it. Demo mode starts and renders a full queue with no credentials present at all. Do not build yet: Nothing after this. ### Out of scope (and why) - Posting, commenting, voting or messaging through the API, which is the line between a research tool and the behaviour that costs people their accounts. - Multiple accounts, rotation and proxies. The asset the paid products actually sell is a handle with years on it, and no code manufactures time. - Unsolicited direct messages, which are the fastest route to a site wide ban and are unwelcome even when they work. - Sentiment dashboards and competitor tracking, which need far more data than one product's mentions and are a different product. ### README must contain - That the tool never posts, and that a person posts from an account they own and read - How to register a script application and obtain a refresh token, and which scopes it needs - That the poll interval must be set against the currently published rate limit, and where to read that limit - That the daily spend ceiling is checked by the scoring loop, and what the queue shows when it is reached - That an unreviewed subreddit produces no drafts until a person records its policy with a date ===== AGENTS.md ===== # Agent instructions · Bazzly indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One process with a scheduled poll on a machine that stays awake, Reddit's OAuth data API through a registered script application, SQLite, one row per thing the API returned, keyed on its fullname, A local keyword prefilter, then one model call per survivor, A server-rendered queue on localhost with copy, dismiss and mark-posted, A daily cost ceiling checked by the scoring loop, not a warning in a log. 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 - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ===== BUILD_PLAN.md ===== # Build plan · Bazzly A Reddit listener that finds threads worth replying to and drafts the reply, whose hard problems are staying inside the documented API and its published limits, keeping model spend below the subscription it replaces, and drafting something that does not invent a feature. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading Reddit the documented way The listener polls inside the published limits and never counts the same thread twice. ### Steps 1. Register a script application and store the refresh token The documented route, with a credential you can revoke without changing your password. Files: `src/reddit/auth.py`, `.env.example` 2. Key everything on the fullname Titles change, links get crossposted, identifiers do not. Files: `src/reddit/poll.py`, `src/db/schema.sql` 3. Obey the headers, not a hard-coded number Record remaining, reset and retry-after on every call so throttling is visible. Files: `src/reddit/client.py`, `tests/test_backoff.py` ### Done when - [ ] A repeated poll produces no duplicates - [ ] Every call is logged with its rate limit headers - [ ] A 429 waits the interval the response specifies - [ ] A run without credentials fails readably ### Watch out - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. ## Phase 2 · Two stages, because the model is the bill Only plausible candidates reach a model call and the day's spend has a ceiling. ### Steps 1. Prefilter locally and count what survives The cheapest call is the one you did not make. Files: `src/score/prefilter.py`, `tests/test_prefilter.py` 2. Demand strict JSON and reject anything else An unparseable answer leaves the row unscored, never scored zero. Files: `src/score/model.py` 3. Enforce the ceiling in the loop A spend limit that only logs is a spend limit that does nothing. Files: `src/score/budget.py`, `tests/test_budget.py` ### Done when - [ ] A fixed fraction of a fixture reaches the model - [ ] Cost and tokens are recorded per call - [ ] The ceiling stops scoring and shows why - [ ] A malformed response leaves the row unscored ### Watch out - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. ## Phase 3 · Drafts that cannot invent a feature A draft only makes claims traceable to a fact sheet you wrote. ### Steps 1. Write the fact sheet before the prompt It is the only source a claim is allowed to come from. Files: `data/facts.yaml`, `src/draft/prompt.py` 2. Mark untraceable claims in a second pass Highlighted in the queue, so the person reads them before copying. Files: `src/draft/verify.py`, `tests/test_claims.py` 3. Make disclosure non-optional Asserted in the suite, not left to the prompt's good behaviour. Files: `src/draft/prompt.py`, `tests/test_disclosure.py` ### Done when - [ ] An unsupported claim is flagged - [ ] Every draft discloses authorship - [ ] Drafts respect the word count - [ ] No fact sheet means no drafting ### Watch out - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. ## Phase 4 · Community rules as data A subreddit that forbids promotion never produces a draft. ### Steps 1. Fetch and store the rules text with its date Stale rules are worse than none, so the date is shown. Files: `src/reddit/rules.py` 2. Default unreviewed to never The safe default is the one that costs you nothing but a review. Files: `src/policy.py`, `tests/test_policy.py` 3. Render the rules beside the draft The person copying the text should read them at that moment. Files: `src/templates/queue.html` ### Done when - [ ] Unreviewed subreddits yield no drafts - [ ] Rules text is stored with a fetch date - [ ] A never policy suppresses drafting entirely - [ ] Policy changes apply without a code change ### Watch out - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ## Not in this build - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. ## After v1, if you want it - A second source such as Hacker News or a forum with a published API, added as another poller behind the same scorer - A weekly digest email of the top candidates, which the queue already has the data for - Per-subreddit tuning of the prefilter, once the score rows show where the wasted calls are going ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Candidates, scores, decisions and the call log. DATABASE_PATH=./data/radar.db # Required. From your script application's settings. REDDIT_CLIENT_ID=a1B2c3D4e5F6g7 # Required · secret. Revocable on its own, which is the point of a script application. REDDIT_CLIENT_SECRET=sk-...redacted # Required · secret. Revoking this is the test for the pause behaviour in the last phase. REDDIT_REFRESH_TOKEN=sk-...redacted # Required. Names the tool and the account. An anonymous agent is a blocked agent. REDDIT_USER_AGENT=reddit-radar/0.1 by your-username # Required · secret. Scoring and drafting. Every call's cost is recorded against it. LLM_API_KEY=sk-...redacted # Required. Checked by the scoring loop. Reaching it stops scoring for the day. DAILY_COST_CEILING_PENCE=50 # Required. Set against the currently published rate limit, not against this example. POLL_INTERVAL_SECONDS=900
You are building a lean indie version of Bazzly. 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 ===== # Bazzly · indie build A Reddit listener that finds threads worth replying to and drafts the reply, whose hard problems are staying inside the documented API and its published limits, keeping model spend below the subscription it replaces, and drafting something that does not invent a feature. 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 | One process with a scheduled poll on a machine that stays awake | a thread's useful window is its first few hours and a laptop lid closes | | API access | Reddit's OAuth data API through a registered script application | the documented route, with a credential you can revoke in one click | | Storage | SQLite, one row per thing the API returned, keyed on its fullname | dedup on the platform's own identifier, never on a title somebody can edit | | Scoring | A local keyword prefilter, then one model call per survivor | scoring every post in a busy subreddit costs more per month than the product | | Review | A server-rendered queue on localhost with copy, dismiss and mark-posted | the output is a draft in your clipboard, not an action on the network | | Spend | A daily cost ceiling checked by the scoring loop, not a warning in a log | the bill is the failure nobody notices until it arrives | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the poller, the scorer and the review page in one process. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **A Reddit account you actually use, with a registered script application** · free - Why: The only account this build ever touches, and the one a person will post from by hand. - Get it: Create a script-type application in your account's app preferences and note the client id, secret and redirect. - Verify: A token request succeeds and an authenticated listing returns with rate limit headers - [ ] **An API key for a text model** · a few pounds a month at the ceiling suggested here - Why: Relevance scoring and drafting both go through it, and it is the only recurring cost in the build. - Get it: Any provider whose pricing you can read per token. - Verify: One call returns valid JSON and its cost is recorded - [ ] **A one page fact sheet for your product** · free - Why: It is the only source a draft may make a claim from, so without it the drafting step is disabled rather than creative. - Get it: Write what the product does, what it costs, and three things it does not do. - Verify: Every claim in a generated draft maps to a line in it - [ ] **A machine that stays awake** (optional) · a few pounds a month, or free if you own the box - Why: A thread is worth answering in its first hours, and a laptop that sleeps turns the listener into a weekly digest. - Get it: A small always-on box, a virtual machine, or a home server. - Verify: A poll scheduled for four in the morning has rows by breakfast - [ ] **Your subreddit policies and your daily spend ceiling** · free - Why: Both are asserted by tests, and deciding them after seeing a bill or a ban means choosing whatever already happened. - Get it: Write the promotion policy for each subreddit you watch, with the date you read its rules, and pick a ceiling in pennies per day. - Verify: Both appear in configuration and are named in the 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: - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. - Aged, high-karma accounts with credible posting history - Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way - Their curated map of which subreddits tolerate product mentions and which will nuke you - A hosted dashboard, team seats, and shared approval queues - Someone else's problem when the API terms or rate limits change If one of those is essential to you, that is the reason to keep paying for Bazzly, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bazzly 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 listener that finds threads worth answering and drafts the reply for you to post yourself to replace Bazzly. 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 Nothing in this build posts to Reddit. Not a comment, not a vote, not a message. It finds threads, scores them and drafts a reply into a box you edit, and a human presses post from an account that human owns and reads. The reason is not squeamishness. The paid products in this category sell aged handles with karma already on them, and an automated reply from an account nobody lives in is the behaviour Reddit removes accounts for, so the feature you would be building is a way to lose the account you are promoting from. The second rule follows from the first. One account, yours, reached through a registered application with credentials you can revoke, using the documented API with a User-Agent that says who you are. No browser automation of a logged-in session, no rotation, no proxies. If what you were buying was account inventory, this build does not replace it and no code does, because the scarce thing is six years of somebody being a person on a website. ### Stack (fixed, do not substitute) - Runtime: One process with a scheduled poll on a machine that stays awake · a thread's useful window is its first few hours and a laptop lid closes - API access: Reddit's OAuth data API through a registered script application · the documented route, with a credential you can revoke in one click - Storage: SQLite, one row per thing the API returned, keyed on its fullname · dedup on the platform's own identifier, never on a title somebody can edit - Scoring: A local keyword prefilter, then one model call per survivor · scoring every post in a busy subreddit costs more per month than the product - Review: A server-rendered queue on localhost with copy, dismiss and mark-posted · the output is a draft in your clipboard, not an action on the network - Spend: A daily cost ceiling checked by the scoring loop, not a warning in a log · the bill is the failure nobody notices until it arrives ### Data model (create this before Phase 1) - subreddits(name, rules_fetched_at, promo_policy, allows_links, reviewed_by, reviewed_at) · what each community permits, recorded by a person - seen(fullname, kind, subreddit, created_utc, first_seen_at) · the dedup table, keyed on Reddit's own identifier - candidates(fullname, permalink, title, body_excerpt, author, subreddit, matched_terms_json, prefilter_score) - scores(fullname, model, relevance, asking_for_recommendation, reason, draft_reply, tokens_in, tokens_out, cost_cents, at) - decisions(fullname, action, acted_at, note) · queued, dismissed, irrelevant, posted_by_hand - api_calls(at, endpoint, status, ratelimit_remaining, ratelimit_reset, retry_after) · the evidence when you get throttled - facts(id, claim, source) · what your product actually does, and the only claims a draft is allowed to make - There is no posts table and no send function, because this build never writes anything to Reddit. ### Phase 1 · Reading Reddit the documented way Build: Register a script application, obtain a refresh token, and set a User-Agent naming the tool and your account. Poll each watched subreddit's new listing and each search term on a schedule. Record every call with the rate limit headers the response carried, back off on a 429 for the interval the response asks for, and read the currently published rate limit before choosing a poll interval rather than copying a number out of a blog post. Trap: Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. Done when: Running the poll twice in succession produces no duplicate candidates, tested back to back against a fixture. Every call is recorded with its status and the rate limit headers it returned. A 429 waits for the interval the response specifies rather than a fixed sleep, tested with a stubbed response. The User-Agent identifies the tool and the account, and a run with no credentials fails with a readable message instead of an anonymous request. Do not build yet: No scoring, no drafting, no review queue. ### Phase 2 · Two stages, because the model is the bill Build: Filter locally first on required terms, excluded terms, minimum body length, post age and score. Send only survivors to one structured model call returning strict JSON: a relevance figure, whether the author is actually asking for a recommendation, a one line reason and a draft. Price every call as you make it and stop scoring for the day when the ceiling is hit. Trap: Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. Done when: A run over a fixture of two hundred posts sends fewer than a stated fraction to the model and the fraction is asserted. Every score row carries its token counts and its computed cost. Reaching the daily ceiling stops scoring and says so on the queue, tested by setting the ceiling to zero. A malformed model response is rejected and retried once, leaving the candidate unscored rather than scored as zero. Do not build yet: No drafting rules, no community policies, no review interface. ### Phase 3 · Drafts that cannot invent a feature Build: Keep a short fact sheet: what the product does, what it costs, what it does not do. Give the model the thread, the fact sheet and a sample of your own writing. Run a second pass marking any sentence whose claim is not traceable to a fact, and show those highlighted in the queue. Require every draft to say plainly that you are the person who built the thing. Trap: A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. Done when: A draft referencing a capability absent from the fact sheet is flagged, tested with a sheet that omits a feature the thread asks about. Every draft contains the disclosure that you built the product, asserted in the suite. Drafts stay inside a stated word count. Removing the fact sheet stops drafting with a readable reason rather than drafting from nothing. Do not build yet: No community policies and no review interface yet. ### Phase 4 · Community rules as data Build: Record a policy per subreddit: promotion allowed, replies only when asked, or never. Fetch each subreddit's rules through the API and store the text beside your judgement with the date. Show the rules next to the draft. Default an unreviewed subreddit to never, and make reviewing it an explicit action with a name and a date attached. Trap: Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. Done when: An unreviewed subreddit produces candidates but no drafts, tested. Rules text is fetched, stored with its fetch date and rendered beside the draft. A subreddit marked never produces no draft at all under any relevance score. Changing a policy takes effect on the next run without editing code. Do not build yet: No review interface, export or demo mode yet. ### Phase 5 · A queue you work, and a key you can kill Build: A review queue sorted by relevance showing thread, reason, policy and an editable draft, with copy, dismiss and mark-posted all recorded. Pause everything on the first authentication failure with one alert instead of failing every poll. Export the history as CSV. A demo mode runs on fixture data with no credentials. Bind to localhost and keep every secret in the environment. Trap: A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. Done when: The queue shows thread, score, reason, subreddit policy and an editable draft for every candidate. Marking a thread posted records when and never shows it again. Revoking the token pauses the listener within one poll and states the reason on the page, tested by revoking it. Demo mode starts and renders a full queue with no credentials present at all. Do not build yet: Nothing after this. ### Out of scope (and why) - Posting, commenting, voting or messaging through the API, which is the line between a research tool and the behaviour that costs people their accounts. - Multiple accounts, rotation and proxies. The asset the paid products actually sell is a handle with years on it, and no code manufactures time. - Unsolicited direct messages, which are the fastest route to a site wide ban and are unwelcome even when they work. - Sentiment dashboards and competitor tracking, which need far more data than one product's mentions and are a different product. ### README must contain - That the tool never posts, and that a person posts from an account they own and read - How to register a script application and obtain a refresh token, and which scopes it needs - That the poll interval must be set against the currently published rate limit, and where to read that limit - That the daily spend ceiling is checked by the scoring loop, and what the queue shows when it is reached - That an unreviewed subreddit produces no drafts until a person records its policy with a date ===== AGENTS.md ===== # Agent instructions · Bazzly indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One process with a scheduled poll on a machine that stays awake, Reddit's OAuth data API through a registered script application, SQLite, one row per thing the API returned, keyed on its fullname, A local keyword prefilter, then one model call per survivor, A server-rendered queue on localhost with copy, dismiss and mark-posted, A daily cost ceiling checked by the scoring loop, not a warning in a log. 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 - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ===== BUILD_PLAN.md ===== # Build plan · Bazzly A Reddit listener that finds threads worth replying to and drafts the reply, whose hard problems are staying inside the documented API and its published limits, keeping model spend below the subscription it replaces, and drafting something that does not invent a feature. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading Reddit the documented way The listener polls inside the published limits and never counts the same thread twice. ### Steps 1. Register a script application and store the refresh token The documented route, with a credential you can revoke without changing your password. Files: `src/reddit/auth.py`, `.env.example` 2. Key everything on the fullname Titles change, links get crossposted, identifiers do not. Files: `src/reddit/poll.py`, `src/db/schema.sql` 3. Obey the headers, not a hard-coded number Record remaining, reset and retry-after on every call so throttling is visible. Files: `src/reddit/client.py`, `tests/test_backoff.py` ### Done when - [ ] A repeated poll produces no duplicates - [ ] Every call is logged with its rate limit headers - [ ] A 429 waits the interval the response specifies - [ ] A run without credentials fails readably ### Watch out - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. ## Phase 2 · Two stages, because the model is the bill Only plausible candidates reach a model call and the day's spend has a ceiling. ### Steps 1. Prefilter locally and count what survives The cheapest call is the one you did not make. Files: `src/score/prefilter.py`, `tests/test_prefilter.py` 2. Demand strict JSON and reject anything else An unparseable answer leaves the row unscored, never scored zero. Files: `src/score/model.py` 3. Enforce the ceiling in the loop A spend limit that only logs is a spend limit that does nothing. Files: `src/score/budget.py`, `tests/test_budget.py` ### Done when - [ ] A fixed fraction of a fixture reaches the model - [ ] Cost and tokens are recorded per call - [ ] The ceiling stops scoring and shows why - [ ] A malformed response leaves the row unscored ### Watch out - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. ## Phase 3 · Drafts that cannot invent a feature A draft only makes claims traceable to a fact sheet you wrote. ### Steps 1. Write the fact sheet before the prompt It is the only source a claim is allowed to come from. Files: `data/facts.yaml`, `src/draft/prompt.py` 2. Mark untraceable claims in a second pass Highlighted in the queue, so the person reads them before copying. Files: `src/draft/verify.py`, `tests/test_claims.py` 3. Make disclosure non-optional Asserted in the suite, not left to the prompt's good behaviour. Files: `src/draft/prompt.py`, `tests/test_disclosure.py` ### Done when - [ ] An unsupported claim is flagged - [ ] Every draft discloses authorship - [ ] Drafts respect the word count - [ ] No fact sheet means no drafting ### Watch out - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. ## Phase 4 · Community rules as data A subreddit that forbids promotion never produces a draft. ### Steps 1. Fetch and store the rules text with its date Stale rules are worse than none, so the date is shown. Files: `src/reddit/rules.py` 2. Default unreviewed to never The safe default is the one that costs you nothing but a review. Files: `src/policy.py`, `tests/test_policy.py` 3. Render the rules beside the draft The person copying the text should read them at that moment. Files: `src/templates/queue.html` ### Done when - [ ] Unreviewed subreddits yield no drafts - [ ] Rules text is stored with a fetch date - [ ] A never policy suppresses drafting entirely - [ ] Policy changes apply without a code change ### Watch out - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ## Not in this build - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. ## After v1, if you want it - A second source such as Hacker News or a forum with a published API, added as another poller behind the same scorer - A weekly digest email of the top candidates, which the queue already has the data for - Per-subreddit tuning of the prefilter, once the score rows show where the wasted calls are going ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Candidates, scores, decisions and the call log. DATABASE_PATH=./data/radar.db # Required. From your script application's settings. REDDIT_CLIENT_ID=a1B2c3D4e5F6g7 # Required · secret. Revocable on its own, which is the point of a script application. REDDIT_CLIENT_SECRET=sk-...redacted # Required · secret. Revoking this is the test for the pause behaviour in the last phase. REDDIT_REFRESH_TOKEN=sk-...redacted # Required. Names the tool and the account. An anonymous agent is a blocked agent. REDDIT_USER_AGENT=reddit-radar/0.1 by your-username # Required · secret. Scoring and drafting. Every call's cost is recorded against it. LLM_API_KEY=sk-...redacted # Required. Checked by the scoring loop. Reaching it stops scoring for the day. DAILY_COST_CEILING_PENCE=50 # Required. Set against the currently published rate limit, not against this example. POLL_INTERVAL_SECONDS=900
You are building a production product version of Bazzly. 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 ===== # Bazzly · product brief ## Problem The listening half of this is not hard: Reddit exposes search and subreddit feeds, and an agent can rank hits for relevance and draft a comment in your voice by lunchtime. The half you cannot build is the account layer, meaning aged handles with real karma, subreddit-specific posting history, and enough trust to not get shadowbanned on the first link drop. If you are promoting one product from one account you actually own and post from, the DIY version covers most of the real value. If the reason you were paying is account inventory and rotation, no amount of code fixes that, because the scarce thing is time served on someone else's platform. Also worth saying out loud: the moment you are automating replies from accounts that are not yours, you are doing the thing Reddit bans people for, and no tool makes that safe. ## Product outcome A listener that shows you the Reddit threads worth answering with a draft already written, keeps its model spend under a ceiling you set, never posts anything itself, and goes quiet loudly rather than silently when its credential dies. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Reddit account and a script-type OAuth app with client id and secret - An LLM API key for relevance scoring and drafting - Somewhere to run a cron job, a laptop is fine - Willingness to read Reddit's API terms and each subreddit's self-promo rules ## Explicit non-goals for v1 - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. - Aged, high-karma accounts with credible posting history - Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way - Their curated map of which subreddits tolerate product mentions and which will nuke you - A hosted dashboard, team seats, and shared approval queues - Someone else's problem when the API terms or rate limits change ## Success criteria - No code path is confirmed to write to Reddit - A repeated poll is confirmed to produce no duplicates - The daily ceiling is confirmed to stop scoring - An unsupported claim is confirmed flagged in a draft - A revoked token is confirmed to pause the listener within one poll ===== BRIEF.md ===== # Build brief · Bazzly 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 listener that finds threads worth answering and drafts the reply for you to post yourself to replace Bazzly. 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 Nothing in this build posts to Reddit. Not a comment, not a vote, not a message. It finds threads, scores them and drafts a reply into a box you edit, and a human presses post from an account that human owns and reads. The reason is not squeamishness. The paid products in this category sell aged handles with karma already on them, and an automated reply from an account nobody lives in is the behaviour Reddit removes accounts for, so the feature you would be building is a way to lose the account you are promoting from. The second rule follows from the first. One account, yours, reached through a registered application with credentials you can revoke, using the documented API with a User-Agent that says who you are. No browser automation of a logged-in session, no rotation, no proxies. If what you were buying was account inventory, this build does not replace it and no code does, because the scarce thing is six years of somebody being a person on a website. ### Stack (fixed, do not substitute) - Runtime: One process with a scheduled poll on a machine that stays awake · a thread's useful window is its first few hours and a laptop lid closes - API access: Reddit's OAuth data API through a registered script application · the documented route, with a credential you can revoke in one click - Storage: SQLite, one row per thing the API returned, keyed on its fullname · dedup on the platform's own identifier, never on a title somebody can edit - Scoring: A local keyword prefilter, then one model call per survivor · scoring every post in a busy subreddit costs more per month than the product - Review: A server-rendered queue on localhost with copy, dismiss and mark-posted · the output is a draft in your clipboard, not an action on the network - Spend: A daily cost ceiling checked by the scoring loop, not a warning in a log · the bill is the failure nobody notices until it arrives ### Data model (create this before Phase 1) - subreddits(name, rules_fetched_at, promo_policy, allows_links, reviewed_by, reviewed_at) · what each community permits, recorded by a person - seen(fullname, kind, subreddit, created_utc, first_seen_at) · the dedup table, keyed on Reddit's own identifier - candidates(fullname, permalink, title, body_excerpt, author, subreddit, matched_terms_json, prefilter_score) - scores(fullname, model, relevance, asking_for_recommendation, reason, draft_reply, tokens_in, tokens_out, cost_cents, at) - decisions(fullname, action, acted_at, note) · queued, dismissed, irrelevant, posted_by_hand - api_calls(at, endpoint, status, ratelimit_remaining, ratelimit_reset, retry_after) · the evidence when you get throttled - facts(id, claim, source) · what your product actually does, and the only claims a draft is allowed to make - There is no posts table and no send function, because this build never writes anything to Reddit. ### Phase 1 · Reading Reddit the documented way Build: Register a script application, obtain a refresh token, and set a User-Agent naming the tool and your account. Poll each watched subreddit's new listing and each search term on a schedule. Record every call with the rate limit headers the response carried, back off on a 429 for the interval the response asks for, and read the currently published rate limit before choosing a poll interval rather than copying a number out of a blog post. Trap: Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. Done when: Running the poll twice in succession produces no duplicate candidates, tested back to back against a fixture. Every call is recorded with its status and the rate limit headers it returned. A 429 waits for the interval the response specifies rather than a fixed sleep, tested with a stubbed response. The User-Agent identifies the tool and the account, and a run with no credentials fails with a readable message instead of an anonymous request. Do not build yet: No scoring, no drafting, no review queue. ### Phase 2 · Two stages, because the model is the bill Build: Filter locally first on required terms, excluded terms, minimum body length, post age and score. Send only survivors to one structured model call returning strict JSON: a relevance figure, whether the author is actually asking for a recommendation, a one line reason and a draft. Price every call as you make it and stop scoring for the day when the ceiling is hit. Trap: Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. Done when: A run over a fixture of two hundred posts sends fewer than a stated fraction to the model and the fraction is asserted. Every score row carries its token counts and its computed cost. Reaching the daily ceiling stops scoring and says so on the queue, tested by setting the ceiling to zero. A malformed model response is rejected and retried once, leaving the candidate unscored rather than scored as zero. Do not build yet: No drafting rules, no community policies, no review interface. ### Phase 3 · Drafts that cannot invent a feature Build: Keep a short fact sheet: what the product does, what it costs, what it does not do. Give the model the thread, the fact sheet and a sample of your own writing. Run a second pass marking any sentence whose claim is not traceable to a fact, and show those highlighted in the queue. Require every draft to say plainly that you are the person who built the thing. Trap: A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. Done when: A draft referencing a capability absent from the fact sheet is flagged, tested with a sheet that omits a feature the thread asks about. Every draft contains the disclosure that you built the product, asserted in the suite. Drafts stay inside a stated word count. Removing the fact sheet stops drafting with a readable reason rather than drafting from nothing. Do not build yet: No community policies and no review interface yet. ### Phase 4 · Community rules as data Build: Record a policy per subreddit: promotion allowed, replies only when asked, or never. Fetch each subreddit's rules through the API and store the text beside your judgement with the date. Show the rules next to the draft. Default an unreviewed subreddit to never, and make reviewing it an explicit action with a name and a date attached. Trap: Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. Done when: An unreviewed subreddit produces candidates but no drafts, tested. Rules text is fetched, stored with its fetch date and rendered beside the draft. A subreddit marked never produces no draft at all under any relevance score. Changing a policy takes effect on the next run without editing code. Do not build yet: No review interface, export or demo mode yet. ### Phase 5 · A queue you work, and a key you can kill Build: A review queue sorted by relevance showing thread, reason, policy and an editable draft, with copy, dismiss and mark-posted all recorded. Pause everything on the first authentication failure with one alert instead of failing every poll. Export the history as CSV. A demo mode runs on fixture data with no credentials. Bind to localhost and keep every secret in the environment. Trap: A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. Done when: The queue shows thread, score, reason, subreddit policy and an editable draft for every candidate. Marking a thread posted records when and never shows it again. Revoking the token pauses the listener within one poll and states the reason on the page, tested by revoking it. Demo mode starts and renders a full queue with no credentials present at all. Do not build yet: Nothing after this. ### Out of scope (and why) - Posting, commenting, voting or messaging through the API, which is the line between a research tool and the behaviour that costs people their accounts. - Multiple accounts, rotation and proxies. The asset the paid products actually sell is a handle with years on it, and no code manufactures time. - Unsolicited direct messages, which are the fastest route to a site wide ban and are unwelcome even when they work. - Sentiment dashboards and competitor tracking, which need far more data than one product's mentions and are a different product. ### README must contain - That the tool never posts, and that a person posts from an account they own and read - How to register a script application and obtain a refresh token, and which scopes it needs - That the poll interval must be set against the currently published rate limit, and where to read that limit - That the daily spend ceiling is checked by the scoring loop, and what the queue shows when it is reached - That an unreviewed subreddit produces no drafts until a person records its policy with a date ===== ARCHITECTURE.md ===== # Architecture · Bazzly ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | One process with a scheduled poll on a machine that stays awake | a thread's useful window is its first few hours and a laptop lid closes | | API access | Reddit's OAuth data API through a registered script application | the documented route, with a credential you can revoke in one click | | Storage | SQLite, one row per thing the API returned, keyed on its fullname | dedup on the platform's own identifier, never on a title somebody can edit | | Scoring | A local keyword prefilter, then one model call per survivor | scoring every post in a busy subreddit costs more per month than the product | | Review | A server-rendered queue on localhost with copy, dismiss and mark-posted | the output is a draft in your clipboard, not an action on the network | | Spend | A daily cost ceiling checked by the scoring loop, not a warning in a log | the bill is the failure nobody notices until it arrives | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Poller | Authenticated listing and search calls with header-aware backoff. | Any client, provided dedup stays on the fullname and headers are honoured. | | Scorer | Local prefilter, one model call, priced and capped. | Any model, provided cost is recorded per call and the ceiling still stops the loop. | | Drafter | Fact-grounded drafts with untraceable claims marked. | Any model, provided the fact sheet remains the only source of claims. | | Policy | Per-subreddit promotion rules with review dates. | Not swappable for a default of allowed, which is how accounts get banned. | | Queue | Review, decisions, export and the paused state. | Any presentation, provided no path from it writes to Reddit. | ## 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 · Candidates, scores, decisions and the call log. - `REDDIT_CLIENT_ID` · required · From your script application's settings. - `REDDIT_CLIENT_SECRET` · required, secret · Revocable on its own, which is the point of a script application. - `REDDIT_REFRESH_TOKEN` · required, secret · Revoking this is the test for the pause behaviour in the last phase. - `REDDIT_USER_AGENT` · required · Names the tool and the account. An anonymous agent is a blocked agent. - `LLM_API_KEY` · required, secret · Scoring and drafting. Every call's cost is recorded against it. - `DAILY_COST_CEILING_PENCE` · required · Checked by the scoring loop. Reaching it stops scoring for the day. - `POLL_INTERVAL_SECONDS` · required · Set against the currently published rate limit, not against this example. ## 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 · Bazzly product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: One process with a scheduled poll on a machine that stays awake, Reddit's OAuth data API through a registered script application, SQLite, one row per thing the API returned, keyed on its fullname, A local keyword prefilter, then one model call per survivor, A server-rendered queue on localhost with copy, dismiss and mark-posted, A daily cost ceiling checked by the scoring loop, not a warning in a log. - 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 - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. - A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. ===== MILESTONES.md ===== # Delivery milestones · Bazzly Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading Reddit the documented way The listener polls inside the published limits and never counts the same thread twice. ### Steps 1. Register a script application and store the refresh token The documented route, with a credential you can revoke without changing your password. Files: `src/reddit/auth.py`, `.env.example` 2. Key everything on the fullname Titles change, links get crossposted, identifiers do not. Files: `src/reddit/poll.py`, `src/db/schema.sql` 3. Obey the headers, not a hard-coded number Record remaining, reset and retry-after on every call so throttling is visible. Files: `src/reddit/client.py`, `tests/test_backoff.py` ### Done when - [ ] A repeated poll produces no duplicates - [ ] Every call is logged with its rate limit headers - [ ] A 429 waits the interval the response specifies - [ ] A run without credentials fails readably ### Watch out - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. ## M2 · Two stages, because the model is the bill Only plausible candidates reach a model call and the day's spend has a ceiling. ### Steps 1. Prefilter locally and count what survives The cheapest call is the one you did not make. Files: `src/score/prefilter.py`, `tests/test_prefilter.py` 2. Demand strict JSON and reject anything else An unparseable answer leaves the row unscored, never scored zero. Files: `src/score/model.py` 3. Enforce the ceiling in the loop A spend limit that only logs is a spend limit that does nothing. Files: `src/score/budget.py`, `tests/test_budget.py` ### Done when - [ ] A fixed fraction of a fixture reaches the model - [ ] Cost and tokens are recorded per call - [ ] The ceiling stops scoring and shows why - [ ] A malformed response leaves the row unscored ### Watch out - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. ## M3 · Drafts that cannot invent a feature A draft only makes claims traceable to a fact sheet you wrote. ### Steps 1. Write the fact sheet before the prompt It is the only source a claim is allowed to come from. Files: `data/facts.yaml`, `src/draft/prompt.py` 2. Mark untraceable claims in a second pass Highlighted in the queue, so the person reads them before copying. Files: `src/draft/verify.py`, `tests/test_claims.py` 3. Make disclosure non-optional Asserted in the suite, not left to the prompt's good behaviour. Files: `src/draft/prompt.py`, `tests/test_disclosure.py` ### Done when - [ ] An unsupported claim is flagged - [ ] Every draft discloses authorship - [ ] Drafts respect the word count - [ ] No fact sheet means no drafting ### Watch out - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. ## M4 · Community rules as data A subreddit that forbids promotion never produces a draft. ### Steps 1. Fetch and store the rules text with its date Stale rules are worse than none, so the date is shown. Files: `src/reddit/rules.py` 2. Default unreviewed to never The safe default is the one that costs you nothing but a review. Files: `src/policy.py`, `tests/test_policy.py` 3. Render the rules beside the draft The person copying the text should read them at that moment. Files: `src/templates/queue.html` ### Done when - [ ] Unreviewed subreddits yield no drafts - [ ] Rules text is stored with a fetch date - [ ] A never policy suppresses drafting entirely - [ ] Policy changes apply without a code change ### Watch out - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ## M5 · A queue you work, and a key you can kill (production only) The queue is usable daily, the credential can be revoked, and the tool demonstrates with no keys at all. ### Steps 1. Pause on the first authentication failure An empty queue should never be ambiguous. Files: `src/reddit/client.py`, `tests/test_pause.py` 2. Record every decision Dismissed and posted are both answers, and both stop it reappearing. Files: `src/db/decisions.py` 3. Ship a demo mode with no keys So the interface can be judged before anyone registers anything. Files: `src/demo/fixtures.json`, `src/app.py` ### Done when - [ ] The queue shows thread, reason, policy and draft - [ ] A posted thread never reappears - [ ] A revoked token pauses within one poll - [ ] Demo mode runs with no credentials ### Watch out - A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. ===== OPERATIONS.md ===== # Operations · Bazzly ## Backup The database holds your review history and your spend record, and both matter more than the candidates. A nightly copy beside the fact sheet and the policy file is enough, since everything else can be re-fetched. ## Restore Restore and run one poll. Because dedup is keyed on the fullname, a restored database simply skips everything it already saw, and the only visible loss is scores for posts that have since fallen out of the listing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Three numbers: calls that returned 429, spend against the ceiling, and time since the last successful poll. The third is the one that catches a dead token, and it belongs on the queue page rather than in a log. ## Incident checklist An empty queue is either a quiet week or a broken listener, and the paused banner is what tells them apart. A spend surprise means the prefilter stopped filtering, and the score rows show exactly which subreddit started flooding. 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 code path is confirmed to write to Reddit - [ ] A repeated poll is confirmed to produce no duplicates - [ ] The daily ceiling is confirmed to stop scoring - [ ] An unsupported claim is confirmed flagged in a draft - [ ] A revoked token is confirmed to pause the listener within one poll ## Launch constraint Do not market omitted Bazzly 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. Candidates, scores, decisions and the call log. DATABASE_PATH=./data/radar.db # Required. From your script application's settings. REDDIT_CLIENT_ID=a1B2c3D4e5F6g7 # Required · secret. Revocable on its own, which is the point of a script application. REDDIT_CLIENT_SECRET=sk-...redacted # Required · secret. Revoking this is the test for the pause behaviour in the last phase. REDDIT_REFRESH_TOKEN=sk-...redacted # Required. Names the tool and the account. An anonymous agent is a blocked agent. REDDIT_USER_AGENT=reddit-radar/0.1 by your-username # Required · secret. Scoring and drafting. Every call's cost is recorded against it. LLM_API_KEY=sk-...redacted # Required. Checked by the scoring loop. Reaching it stops scoring for the day. DAILY_COST_CEILING_PENCE=50 # Required. Set against the currently published rate limit, not against this example. POLL_INTERVAL_SECONDS=900
# Bazzly · indie build A Reddit listener that finds threads worth replying to and drafts the reply, whose hard problems are staying inside the documented API and its published limits, keeping model spend below the subscription it replaces, and drafting something that does not invent a feature. 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 | One process with a scheduled poll on a machine that stays awake | a thread's useful window is its first few hours and a laptop lid closes | | API access | Reddit's OAuth data API through a registered script application | the documented route, with a credential you can revoke in one click | | Storage | SQLite, one row per thing the API returned, keyed on its fullname | dedup on the platform's own identifier, never on a title somebody can edit | | Scoring | A local keyword prefilter, then one model call per survivor | scoring every post in a busy subreddit costs more per month than the product | | Review | A server-rendered queue on localhost with copy, dismiss and mark-posted | the output is a draft in your clipboard, not an action on the network | | Spend | A daily cost ceiling checked by the scoring loop, not a warning in a log | the bill is the failure nobody notices until it arrives | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the poller, the scorer and the review page in one process. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **A Reddit account you actually use, with a registered script application** · free - Why: The only account this build ever touches, and the one a person will post from by hand. - Get it: Create a script-type application in your account's app preferences and note the client id, secret and redirect. - Verify: A token request succeeds and an authenticated listing returns with rate limit headers - [ ] **An API key for a text model** · a few pounds a month at the ceiling suggested here - Why: Relevance scoring and drafting both go through it, and it is the only recurring cost in the build. - Get it: Any provider whose pricing you can read per token. - Verify: One call returns valid JSON and its cost is recorded - [ ] **A one page fact sheet for your product** · free - Why: It is the only source a draft may make a claim from, so without it the drafting step is disabled rather than creative. - Get it: Write what the product does, what it costs, and three things it does not do. - Verify: Every claim in a generated draft maps to a line in it - [ ] **A machine that stays awake** (optional) · a few pounds a month, or free if you own the box - Why: A thread is worth answering in its first hours, and a laptop that sleeps turns the listener into a weekly digest. - Get it: A small always-on box, a virtual machine, or a home server. - Verify: A poll scheduled for four in the morning has rows by breakfast - [ ] **Your subreddit policies and your daily spend ceiling** · free - Why: Both are asserted by tests, and deciding them after seeing a bill or a ban means choosing whatever already happened. - Get it: Write the promotion policy for each subreddit you watch, with the date you read its rules, and pick a ceiling in pennies per day. - Verify: Both appear in configuration and are named in the 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: - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. - Aged, high-karma accounts with credible posting history - Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way - Their curated map of which subreddits tolerate product mentions and which will nuke you - A hosted dashboard, team seats, and shared approval queues - Someone else's problem when the API terms or rate limits change If one of those is essential to you, that is the reason to keep paying for Bazzly, and the README should say so rather than pretend.
# Build brief · Bazzly 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 listener that finds threads worth answering and drafts the reply for you to post yourself to replace Bazzly. 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 Nothing in this build posts to Reddit. Not a comment, not a vote, not a message. It finds threads, scores them and drafts a reply into a box you edit, and a human presses post from an account that human owns and reads. The reason is not squeamishness. The paid products in this category sell aged handles with karma already on them, and an automated reply from an account nobody lives in is the behaviour Reddit removes accounts for, so the feature you would be building is a way to lose the account you are promoting from. The second rule follows from the first. One account, yours, reached through a registered application with credentials you can revoke, using the documented API with a User-Agent that says who you are. No browser automation of a logged-in session, no rotation, no proxies. If what you were buying was account inventory, this build does not replace it and no code does, because the scarce thing is six years of somebody being a person on a website. ### Stack (fixed, do not substitute) - Runtime: One process with a scheduled poll on a machine that stays awake · a thread's useful window is its first few hours and a laptop lid closes - API access: Reddit's OAuth data API through a registered script application · the documented route, with a credential you can revoke in one click - Storage: SQLite, one row per thing the API returned, keyed on its fullname · dedup on the platform's own identifier, never on a title somebody can edit - Scoring: A local keyword prefilter, then one model call per survivor · scoring every post in a busy subreddit costs more per month than the product - Review: A server-rendered queue on localhost with copy, dismiss and mark-posted · the output is a draft in your clipboard, not an action on the network - Spend: A daily cost ceiling checked by the scoring loop, not a warning in a log · the bill is the failure nobody notices until it arrives ### Data model (create this before Phase 1) - subreddits(name, rules_fetched_at, promo_policy, allows_links, reviewed_by, reviewed_at) · what each community permits, recorded by a person - seen(fullname, kind, subreddit, created_utc, first_seen_at) · the dedup table, keyed on Reddit's own identifier - candidates(fullname, permalink, title, body_excerpt, author, subreddit, matched_terms_json, prefilter_score) - scores(fullname, model, relevance, asking_for_recommendation, reason, draft_reply, tokens_in, tokens_out, cost_cents, at) - decisions(fullname, action, acted_at, note) · queued, dismissed, irrelevant, posted_by_hand - api_calls(at, endpoint, status, ratelimit_remaining, ratelimit_reset, retry_after) · the evidence when you get throttled - facts(id, claim, source) · what your product actually does, and the only claims a draft is allowed to make - There is no posts table and no send function, because this build never writes anything to Reddit. ### Phase 1 · Reading Reddit the documented way Build: Register a script application, obtain a refresh token, and set a User-Agent naming the tool and your account. Poll each watched subreddit's new listing and each search term on a schedule. Record every call with the rate limit headers the response carried, back off on a 429 for the interval the response asks for, and read the currently published rate limit before choosing a poll interval rather than copying a number out of a blog post. Trap: Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. Done when: Running the poll twice in succession produces no duplicate candidates, tested back to back against a fixture. Every call is recorded with its status and the rate limit headers it returned. A 429 waits for the interval the response specifies rather than a fixed sleep, tested with a stubbed response. The User-Agent identifies the tool and the account, and a run with no credentials fails with a readable message instead of an anonymous request. Do not build yet: No scoring, no drafting, no review queue. ### Phase 2 · Two stages, because the model is the bill Build: Filter locally first on required terms, excluded terms, minimum body length, post age and score. Send only survivors to one structured model call returning strict JSON: a relevance figure, whether the author is actually asking for a recommendation, a one line reason and a draft. Price every call as you make it and stop scoring for the day when the ceiling is hit. Trap: Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. Done when: A run over a fixture of two hundred posts sends fewer than a stated fraction to the model and the fraction is asserted. Every score row carries its token counts and its computed cost. Reaching the daily ceiling stops scoring and says so on the queue, tested by setting the ceiling to zero. A malformed model response is rejected and retried once, leaving the candidate unscored rather than scored as zero. Do not build yet: No drafting rules, no community policies, no review interface. ### Phase 3 · Drafts that cannot invent a feature Build: Keep a short fact sheet: what the product does, what it costs, what it does not do. Give the model the thread, the fact sheet and a sample of your own writing. Run a second pass marking any sentence whose claim is not traceable to a fact, and show those highlighted in the queue. Require every draft to say plainly that you are the person who built the thing. Trap: A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. Done when: A draft referencing a capability absent from the fact sheet is flagged, tested with a sheet that omits a feature the thread asks about. Every draft contains the disclosure that you built the product, asserted in the suite. Drafts stay inside a stated word count. Removing the fact sheet stops drafting with a readable reason rather than drafting from nothing. Do not build yet: No community policies and no review interface yet. ### Phase 4 · Community rules as data Build: Record a policy per subreddit: promotion allowed, replies only when asked, or never. Fetch each subreddit's rules through the API and store the text beside your judgement with the date. Show the rules next to the draft. Default an unreviewed subreddit to never, and make reviewing it an explicit action with a name and a date attached. Trap: Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. Done when: An unreviewed subreddit produces candidates but no drafts, tested. Rules text is fetched, stored with its fetch date and rendered beside the draft. A subreddit marked never produces no draft at all under any relevance score. Changing a policy takes effect on the next run without editing code. Do not build yet: No review interface, export or demo mode yet. ### Phase 5 · A queue you work, and a key you can kill Build: A review queue sorted by relevance showing thread, reason, policy and an editable draft, with copy, dismiss and mark-posted all recorded. Pause everything on the first authentication failure with one alert instead of failing every poll. Export the history as CSV. A demo mode runs on fixture data with no credentials. Bind to localhost and keep every secret in the environment. Trap: A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. Done when: The queue shows thread, score, reason, subreddit policy and an editable draft for every candidate. Marking a thread posted records when and never shows it again. Revoking the token pauses the listener within one poll and states the reason on the page, tested by revoking it. Demo mode starts and renders a full queue with no credentials present at all. Do not build yet: Nothing after this. ### Out of scope (and why) - Posting, commenting, voting or messaging through the API, which is the line between a research tool and the behaviour that costs people their accounts. - Multiple accounts, rotation and proxies. The asset the paid products actually sell is a handle with years on it, and no code manufactures time. - Unsolicited direct messages, which are the fastest route to a site wide ban and are unwelcome even when they work. - Sentiment dashboards and competitor tracking, which need far more data than one product's mentions and are a different product. ### README must contain - That the tool never posts, and that a person posts from an account they own and read - How to register a script application and obtain a refresh token, and which scopes it needs - That the poll interval must be set against the currently published rate limit, and where to read that limit - That the daily spend ceiling is checked by the scoring loop, and what the queue shows when it is reached - That an unreviewed subreddit produces no drafts until a person records its policy with a date
# Agent instructions · Bazzly indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One process with a scheduled poll on a machine that stays awake, Reddit's OAuth data API through a registered script application, SQLite, one row per thing the API returned, keyed on its fullname, A local keyword prefilter, then one model call per survivor, A server-rendered queue on localhost with copy, dismiss and mark-posted, A daily cost ceiling checked by the scoring loop, not a warning in a log. 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 - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when.
# Build plan · Bazzly A Reddit listener that finds threads worth replying to and drafts the reply, whose hard problems are staying inside the documented API and its published limits, keeping model spend below the subscription it replaces, and drafting something that does not invent a feature. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading Reddit the documented way The listener polls inside the published limits and never counts the same thread twice. ### Steps 1. Register a script application and store the refresh token The documented route, with a credential you can revoke without changing your password. Files: `src/reddit/auth.py`, `.env.example` 2. Key everything on the fullname Titles change, links get crossposted, identifiers do not. Files: `src/reddit/poll.py`, `src/db/schema.sql` 3. Obey the headers, not a hard-coded number Record remaining, reset and retry-after on every call so throttling is visible. Files: `src/reddit/client.py`, `tests/test_backoff.py` ### Done when - [ ] A repeated poll produces no duplicates - [ ] Every call is logged with its rate limit headers - [ ] A 429 waits the interval the response specifies - [ ] A run without credentials fails readably ### Watch out - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. ## Phase 2 · Two stages, because the model is the bill Only plausible candidates reach a model call and the day's spend has a ceiling. ### Steps 1. Prefilter locally and count what survives The cheapest call is the one you did not make. Files: `src/score/prefilter.py`, `tests/test_prefilter.py` 2. Demand strict JSON and reject anything else An unparseable answer leaves the row unscored, never scored zero. Files: `src/score/model.py` 3. Enforce the ceiling in the loop A spend limit that only logs is a spend limit that does nothing. Files: `src/score/budget.py`, `tests/test_budget.py` ### Done when - [ ] A fixed fraction of a fixture reaches the model - [ ] Cost and tokens are recorded per call - [ ] The ceiling stops scoring and shows why - [ ] A malformed response leaves the row unscored ### Watch out - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. ## Phase 3 · Drafts that cannot invent a feature A draft only makes claims traceable to a fact sheet you wrote. ### Steps 1. Write the fact sheet before the prompt It is the only source a claim is allowed to come from. Files: `data/facts.yaml`, `src/draft/prompt.py` 2. Mark untraceable claims in a second pass Highlighted in the queue, so the person reads them before copying. Files: `src/draft/verify.py`, `tests/test_claims.py` 3. Make disclosure non-optional Asserted in the suite, not left to the prompt's good behaviour. Files: `src/draft/prompt.py`, `tests/test_disclosure.py` ### Done when - [ ] An unsupported claim is flagged - [ ] Every draft discloses authorship - [ ] Drafts respect the word count - [ ] No fact sheet means no drafting ### Watch out - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. ## Phase 4 · Community rules as data A subreddit that forbids promotion never produces a draft. ### Steps 1. Fetch and store the rules text with its date Stale rules are worse than none, so the date is shown. Files: `src/reddit/rules.py` 2. Default unreviewed to never The safe default is the one that costs you nothing but a review. Files: `src/policy.py`, `tests/test_policy.py` 3. Render the rules beside the draft The person copying the text should read them at that moment. Files: `src/templates/queue.html` ### Done when - [ ] Unreviewed subreddits yield no drafts - [ ] Rules text is stored with a fetch date - [ ] A never policy suppresses drafting entirely - [ ] Policy changes apply without a code change ### Watch out - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ## Not in this build - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. ## After v1, if you want it - A second source such as Hacker News or a forum with a published API, added as another poller behind the same scorer - A weekly digest email of the top candidates, which the queue already has the data for - Per-subreddit tuning of the prefilter, once the score rows show where the wasted calls are going
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Candidates, scores, decisions and the call log. DATABASE_PATH=./data/radar.db # Required. From your script application's settings. REDDIT_CLIENT_ID=a1B2c3D4e5F6g7 # Required · secret. Revocable on its own, which is the point of a script application. REDDIT_CLIENT_SECRET=sk-...redacted # Required · secret. Revoking this is the test for the pause behaviour in the last phase. REDDIT_REFRESH_TOKEN=sk-...redacted # Required. Names the tool and the account. An anonymous agent is a blocked agent. REDDIT_USER_AGENT=reddit-radar/0.1 by your-username # Required · secret. Scoring and drafting. Every call's cost is recorded against it. LLM_API_KEY=sk-...redacted # Required. Checked by the scoring loop. Reaching it stops scoring for the day. DAILY_COST_CEILING_PENCE=50 # Required. Set against the currently published rate limit, not against this example. POLL_INTERVAL_SECONDS=900
# Bazzly · product brief ## Problem The listening half of this is not hard: Reddit exposes search and subreddit feeds, and an agent can rank hits for relevance and draft a comment in your voice by lunchtime. The half you cannot build is the account layer, meaning aged handles with real karma, subreddit-specific posting history, and enough trust to not get shadowbanned on the first link drop. If you are promoting one product from one account you actually own and post from, the DIY version covers most of the real value. If the reason you were paying is account inventory and rotation, no amount of code fixes that, because the scarce thing is time served on someone else's platform. Also worth saying out loud: the moment you are automating replies from accounts that are not yours, you are doing the thing Reddit bans people for, and no tool makes that safe. ## Product outcome A listener that shows you the Reddit threads worth answering with a draft already written, keeps its model spend under a ceiling you set, never posts anything itself, and goes quiet loudly rather than silently when its credential dies. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Reddit account and a script-type OAuth app with client id and secret - An LLM API key for relevance scoring and drafting - Somewhere to run a cron job, a laptop is fine - Willingness to read Reddit's API terms and each subreddit's self-promo rules ## Explicit non-goals for v1 - Posting, voting or messaging of any kind. - Multiple accounts, rotation or proxies. - Sentiment and competitor dashboards. - Browser automation of a logged-in session. - Aged, high-karma accounts with credible posting history - Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way - Their curated map of which subreddits tolerate product mentions and which will nuke you - A hosted dashboard, team seats, and shared approval queues - Someone else's problem when the API terms or rate limits change ## Success criteria - No code path is confirmed to write to Reddit - A repeated poll is confirmed to produce no duplicates - The daily ceiling is confirmed to stop scoring - An unsupported claim is confirmed flagged in a draft - A revoked token is confirmed to pause the listener within one poll
# Build brief · Bazzly 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 listener that finds threads worth answering and drafts the reply for you to post yourself to replace Bazzly. 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 Nothing in this build posts to Reddit. Not a comment, not a vote, not a message. It finds threads, scores them and drafts a reply into a box you edit, and a human presses post from an account that human owns and reads. The reason is not squeamishness. The paid products in this category sell aged handles with karma already on them, and an automated reply from an account nobody lives in is the behaviour Reddit removes accounts for, so the feature you would be building is a way to lose the account you are promoting from. The second rule follows from the first. One account, yours, reached through a registered application with credentials you can revoke, using the documented API with a User-Agent that says who you are. No browser automation of a logged-in session, no rotation, no proxies. If what you were buying was account inventory, this build does not replace it and no code does, because the scarce thing is six years of somebody being a person on a website. ### Stack (fixed, do not substitute) - Runtime: One process with a scheduled poll on a machine that stays awake · a thread's useful window is its first few hours and a laptop lid closes - API access: Reddit's OAuth data API through a registered script application · the documented route, with a credential you can revoke in one click - Storage: SQLite, one row per thing the API returned, keyed on its fullname · dedup on the platform's own identifier, never on a title somebody can edit - Scoring: A local keyword prefilter, then one model call per survivor · scoring every post in a busy subreddit costs more per month than the product - Review: A server-rendered queue on localhost with copy, dismiss and mark-posted · the output is a draft in your clipboard, not an action on the network - Spend: A daily cost ceiling checked by the scoring loop, not a warning in a log · the bill is the failure nobody notices until it arrives ### Data model (create this before Phase 1) - subreddits(name, rules_fetched_at, promo_policy, allows_links, reviewed_by, reviewed_at) · what each community permits, recorded by a person - seen(fullname, kind, subreddit, created_utc, first_seen_at) · the dedup table, keyed on Reddit's own identifier - candidates(fullname, permalink, title, body_excerpt, author, subreddit, matched_terms_json, prefilter_score) - scores(fullname, model, relevance, asking_for_recommendation, reason, draft_reply, tokens_in, tokens_out, cost_cents, at) - decisions(fullname, action, acted_at, note) · queued, dismissed, irrelevant, posted_by_hand - api_calls(at, endpoint, status, ratelimit_remaining, ratelimit_reset, retry_after) · the evidence when you get throttled - facts(id, claim, source) · what your product actually does, and the only claims a draft is allowed to make - There is no posts table and no send function, because this build never writes anything to Reddit. ### Phase 1 · Reading Reddit the documented way Build: Register a script application, obtain a refresh token, and set a User-Agent naming the tool and your account. Poll each watched subreddit's new listing and each search term on a schedule. Record every call with the rate limit headers the response carried, back off on a 429 for the interval the response asks for, and read the currently published rate limit before choosing a poll interval rather than copying a number out of a blog post. Trap: Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. Done when: Running the poll twice in succession produces no duplicate candidates, tested back to back against a fixture. Every call is recorded with its status and the rate limit headers it returned. A 429 waits for the interval the response specifies rather than a fixed sleep, tested with a stubbed response. The User-Agent identifies the tool and the account, and a run with no credentials fails with a readable message instead of an anonymous request. Do not build yet: No scoring, no drafting, no review queue. ### Phase 2 · Two stages, because the model is the bill Build: Filter locally first on required terms, excluded terms, minimum body length, post age and score. Send only survivors to one structured model call returning strict JSON: a relevance figure, whether the author is actually asking for a recommendation, a one line reason and a draft. Price every call as you make it and stop scoring for the day when the ceiling is hit. Trap: Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. Done when: A run over a fixture of two hundred posts sends fewer than a stated fraction to the model and the fraction is asserted. Every score row carries its token counts and its computed cost. Reaching the daily ceiling stops scoring and says so on the queue, tested by setting the ceiling to zero. A malformed model response is rejected and retried once, leaving the candidate unscored rather than scored as zero. Do not build yet: No drafting rules, no community policies, no review interface. ### Phase 3 · Drafts that cannot invent a feature Build: Keep a short fact sheet: what the product does, what it costs, what it does not do. Give the model the thread, the fact sheet and a sample of your own writing. Run a second pass marking any sentence whose claim is not traceable to a fact, and show those highlighted in the queue. Require every draft to say plainly that you are the person who built the thing. Trap: A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. Done when: A draft referencing a capability absent from the fact sheet is flagged, tested with a sheet that omits a feature the thread asks about. Every draft contains the disclosure that you built the product, asserted in the suite. Drafts stay inside a stated word count. Removing the fact sheet stops drafting with a readable reason rather than drafting from nothing. Do not build yet: No community policies and no review interface yet. ### Phase 4 · Community rules as data Build: Record a policy per subreddit: promotion allowed, replies only when asked, or never. Fetch each subreddit's rules through the API and store the text beside your judgement with the date. Show the rules next to the draft. Default an unreviewed subreddit to never, and make reviewing it an explicit action with a name and a date attached. Trap: Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. Done when: An unreviewed subreddit produces candidates but no drafts, tested. Rules text is fetched, stored with its fetch date and rendered beside the draft. A subreddit marked never produces no draft at all under any relevance score. Changing a policy takes effect on the next run without editing code. Do not build yet: No review interface, export or demo mode yet. ### Phase 5 · A queue you work, and a key you can kill Build: A review queue sorted by relevance showing thread, reason, policy and an editable draft, with copy, dismiss and mark-posted all recorded. Pause everything on the first authentication failure with one alert instead of failing every poll. Export the history as CSV. A demo mode runs on fixture data with no credentials. Bind to localhost and keep every secret in the environment. Trap: A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once. Done when: The queue shows thread, score, reason, subreddit policy and an editable draft for every candidate. Marking a thread posted records when and never shows it again. Revoking the token pauses the listener within one poll and states the reason on the page, tested by revoking it. Demo mode starts and renders a full queue with no credentials present at all. Do not build yet: Nothing after this. ### Out of scope (and why) - Posting, commenting, voting or messaging through the API, which is the line between a research tool and the behaviour that costs people their accounts. - Multiple accounts, rotation and proxies. The asset the paid products actually sell is a handle with years on it, and no code manufactures time. - Unsolicited direct messages, which are the fastest route to a site wide ban and are unwelcome even when they work. - Sentiment dashboards and competitor tracking, which need far more data than one product's mentions and are a different product. ### README must contain - That the tool never posts, and that a person posts from an account they own and read - How to register a script application and obtain a refresh token, and which scopes it needs - That the poll interval must be set against the currently published rate limit, and where to read that limit - That the daily spend ceiling is checked by the scoring loop, and what the queue shows when it is reached - That an unreviewed subreddit produces no drafts until a person records its policy with a date
# Architecture · Bazzly ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | One process with a scheduled poll on a machine that stays awake | a thread's useful window is its first few hours and a laptop lid closes | | API access | Reddit's OAuth data API through a registered script application | the documented route, with a credential you can revoke in one click | | Storage | SQLite, one row per thing the API returned, keyed on its fullname | dedup on the platform's own identifier, never on a title somebody can edit | | Scoring | A local keyword prefilter, then one model call per survivor | scoring every post in a busy subreddit costs more per month than the product | | Review | A server-rendered queue on localhost with copy, dismiss and mark-posted | the output is a draft in your clipboard, not an action on the network | | Spend | A daily cost ceiling checked by the scoring loop, not a warning in a log | the bill is the failure nobody notices until it arrives | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Poller | Authenticated listing and search calls with header-aware backoff. | Any client, provided dedup stays on the fullname and headers are honoured. | | Scorer | Local prefilter, one model call, priced and capped. | Any model, provided cost is recorded per call and the ceiling still stops the loop. | | Drafter | Fact-grounded drafts with untraceable claims marked. | Any model, provided the fact sheet remains the only source of claims. | | Policy | Per-subreddit promotion rules with review dates. | Not swappable for a default of allowed, which is how accounts get banned. | | Queue | Review, decisions, export and the paused state. | Any presentation, provided no path from it writes to Reddit. | ## 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 · Candidates, scores, decisions and the call log. - `REDDIT_CLIENT_ID` · required · From your script application's settings. - `REDDIT_CLIENT_SECRET` · required, secret · Revocable on its own, which is the point of a script application. - `REDDIT_REFRESH_TOKEN` · required, secret · Revoking this is the test for the pause behaviour in the last phase. - `REDDIT_USER_AGENT` · required · Names the tool and the account. An anonymous agent is a blocked agent. - `LLM_API_KEY` · required, secret · Scoring and drafting. Every call's cost is recorded against it. - `DAILY_COST_CEILING_PENCE` · required · Checked by the scoring loop. Reaching it stops scoring for the day. - `POLL_INTERVAL_SECONDS` · required · Set against the currently published rate limit, not against this example. ## 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 · Bazzly product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: One process with a scheduled poll on a machine that stays awake, Reddit's OAuth data API through a registered script application, SQLite, one row per thing the API returned, keyed on its fullname, A local keyword prefilter, then one model call per survivor, A server-rendered queue on localhost with copy, dismiss and mark-posted, A daily cost ceiling checked by the scoring loop, not a warning in a log. - 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 - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. - A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once.
# Delivery milestones · Bazzly Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading Reddit the documented way The listener polls inside the published limits and never counts the same thread twice. ### Steps 1. Register a script application and store the refresh token The documented route, with a credential you can revoke without changing your password. Files: `src/reddit/auth.py`, `.env.example` 2. Key everything on the fullname Titles change, links get crossposted, identifiers do not. Files: `src/reddit/poll.py`, `src/db/schema.sql` 3. Obey the headers, not a hard-coded number Record remaining, reset and retry-after on every call so throttling is visible. Files: `src/reddit/client.py`, `tests/test_backoff.py` ### Done when - [ ] A repeated poll produces no duplicates - [ ] Every call is logged with its rate limit headers - [ ] A 429 waits the interval the response specifies - [ ] A run without credentials fails readably ### Watch out - Deduping on the title or the link means an edited title, a crosspost and a resubmission all arrive as new, and the queue fills with one thread three times while a genuinely new one scrolls off the end. Key everything on the fullname the API returns, store it before you score it, and treat a fullname you have seen as finished whatever changed about it since. ## M2 · Two stages, because the model is the bill Only plausible candidates reach a model call and the day's spend has a ceiling. ### Steps 1. Prefilter locally and count what survives The cheapest call is the one you did not make. Files: `src/score/prefilter.py`, `tests/test_prefilter.py` 2. Demand strict JSON and reject anything else An unparseable answer leaves the row unscored, never scored zero. Files: `src/score/model.py` 3. Enforce the ceiling in the loop A spend limit that only logs is a spend limit that does nothing. Files: `src/score/budget.py`, `tests/test_budget.py` ### Done when - [ ] A fixed fraction of a fixture reaches the model - [ ] Cost and tokens are recorded per call - [ ] The ceiling stops scoring and shows why - [ ] A malformed response leaves the row unscored ### Watch out - Sending every new post from a busy subreddit to a model looks fine on the first evening and costs more by the end of the month than the subscription you were replacing, and nothing in the interface shows it until the invoice does. Prefilter locally, price each call as it happens, and make the ceiling a condition the scoring loop checks rather than an alert somebody is supposed to read. ## M3 · Drafts that cannot invent a feature A draft only makes claims traceable to a fact sheet you wrote. ### Steps 1. Write the fact sheet before the prompt It is the only source a claim is allowed to come from. Files: `data/facts.yaml`, `src/draft/prompt.py` 2. Mark untraceable claims in a second pass Highlighted in the queue, so the person reads them before copying. Files: `src/draft/verify.py`, `tests/test_claims.py` 3. Make disclosure non-optional Asserted in the suite, not left to the prompt's good behaviour. Files: `src/draft/prompt.py`, `tests/test_disclosure.py` ### Done when - [ ] An unsupported claim is flagged - [ ] Every draft discloses authorship - [ ] Drafts respect the word count - [ ] No fact sheet means no drafting ### Watch out - A model writing a helpful recommendation will confidently attribute a feature you do not have, and because the draft reads well the person posts it, and the correction then happens in public underneath it. Ground the draft in the fact sheet, mark untraceable claims in a second pass, and treat an unflagged draft as the exception rather than the default. ## M4 · Community rules as data A subreddit that forbids promotion never produces a draft. ### Steps 1. Fetch and store the rules text with its date Stale rules are worse than none, so the date is shown. Files: `src/reddit/rules.py` 2. Default unreviewed to never The safe default is the one that costs you nothing but a review. Files: `src/policy.py`, `tests/test_policy.py` 3. Render the rules beside the draft The person copying the text should read them at that moment. Files: `src/templates/queue.html` ### Done when - [ ] Unreviewed subreddits yield no drafts - [ ] Rules text is stored with a fetch date - [ ] A never policy suppresses drafting entirely - [ ] Policy changes apply without a code change ### Watch out - Treating every community the same gets the account banned in the one that bans on a first link, and the ban is not undone by deleting the comment or by explaining that a tool wrote it. Default unreviewed communities to no drafting, keep the rule text beside the draft so the decision is made with it in view, and record who reviewed each policy and when. ## M5 · A queue you work, and a key you can kill (production only) The queue is usable daily, the credential can be revoked, and the tool demonstrates with no keys at all. ### Steps 1. Pause on the first authentication failure An empty queue should never be ambiguous. Files: `src/reddit/client.py`, `tests/test_pause.py` 2. Record every decision Dismissed and posted are both answers, and both stop it reappearing. Files: `src/db/decisions.py` 3. Ship a demo mode with no keys So the interface can be judged before anyone registers anything. Files: `src/demo/fixtures.json`, `src/app.py` ### Done when - [ ] The queue shows thread, reason, policy and draft - [ ] A posted thread never reappears - [ ] A revoked token pauses within one poll - [ ] Demo mode runs with no credentials ### Watch out - A refresh token is revoked or expires and every poll fails quietly for a week, so the first thing you notice is an empty queue, which you read as Reddit being quiet rather than the listener being dead. Pause on the first authentication failure, show the paused state on the queue itself, and alert once.
# Operations · Bazzly ## Backup The database holds your review history and your spend record, and both matter more than the candidates. A nightly copy beside the fact sheet and the policy file is enough, since everything else can be re-fetched. ## Restore Restore and run one poll. Because dedup is keyed on the fullname, a restored database simply skips everything it already saw, and the only visible loss is scores for posts that have since fallen out of the listing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Three numbers: calls that returned 429, spend against the ceiling, and time since the last successful poll. The third is the one that catches a dead token, and it belongs on the queue page rather than in a log. ## Incident checklist An empty queue is either a quiet week or a broken listener, and the paused banner is what tells them apart. A spend surprise means the prefilter stopped filtering, and the score rows show exactly which subreddit started flooding. 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 code path is confirmed to write to Reddit - [ ] A repeated poll is confirmed to produce no duplicates - [ ] The daily ceiling is confirmed to stop scoring - [ ] An unsupported claim is confirmed flagged in a draft - [ ] A revoked token is confirmed to pause the listener within one poll ## Launch constraint Do not market omitted Bazzly 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. Candidates, scores, decisions and the call log. DATABASE_PATH=./data/radar.db # Required. From your script application's settings. REDDIT_CLIENT_ID=a1B2c3D4e5F6g7 # Required · secret. Revocable on its own, which is the point of a script application. REDDIT_CLIENT_SECRET=sk-...redacted # Required · secret. Revoking this is the test for the pause behaviour in the last phase. REDDIT_REFRESH_TOKEN=sk-...redacted # Required. Names the tool and the account. An anonymous agent is a blocked agent. REDDIT_USER_AGENT=reddit-radar/0.1 by your-username # Required · secret. Scoring and drafting. Every call's cost is recorded against it. LLM_API_KEY=sk-...redacted # Required. Checked by the scoring loop. Reaching it stops scoring for the day. DAILY_COST_CEILING_PENCE=50 # Required. Set against the currently published rate limit, not against this example. POLL_INTERVAL_SECONDS=900
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because karma cannot be compiled. People pay for the part of the product that is a stockpile of trusted accounts and a running list of which communities will let a link live for more than eleven minutes, and both of those are accumulated rather than built. There is also the boring reason: the DIY build gives you a feed of opportunities, and most people do not want a feed, they want the comment already posted, by something that looks like a person who has been on Reddit for six years.
xAged, high-karma accounts with credible posting history
xMulti-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way
xTheir curated map of which subreddits tolerate product mentions and which will nuke you
xA hosted dashboard, team seats, and shared approval queues
xSomeone else's problem when the API terms or rate limits change
Nothing worth pointing at. That's why the prompt exists.
Vibecode Bazzly
Kinda. The core of Bazzly is buildable in a weekend with the prompt on this page, but there are real gaps: Aged, high-karma accounts with credible posting history, Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way. Read the honest list above before committing.
How much does Bazzly cost?
Bazzly costs about $99/month (All in One (single plan), checked 2026-08-18), which is $1188 per year.
What do I lose by replacing Bazzly?
Honestly: Aged, high-karma accounts with credible posting history; Multi-account rotation, proxies, and whatever ban-avoidance heuristics they have learned the hard way; Their curated map of which subreddits tolerate product mentions and which will nuke you; A hosted dashboard, team seats, and shared approval queues; Someone else's problem when the API terms or rate limits change. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Bazzly?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.