Vibecode StocksBrew
track this build4 phases, 12 steps, beginner friendly0%A personal stock-research cockpit is very buildable, but StocksBrew's useful current calls depend on continuously maintained market, filings, news, and alert pipelines. A DIY version can make its evidence and rules explicit; it will not inherit the service's coverage, refresh reliability, or editorial judgment.
You are building a lean indie version of StocksBrew. 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 ===== # StocksBrew · indie build A personal research cockpit whose hard problems are producing a stance that is nothing more than a visible sum of stated rules, keeping every displayed number tied to a source and a retrieval time, and fitting a daily refresh inside a data quota that is far smaller than the watchlist wants to be. Estimated effort: **multi-day**. 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 | Node with a server-rendered interface on the loopback address, plus a scheduled refresh | one reader, no accounts, and nothing that could be mistaken for a publication | | Storage | SQLite, one file, with every fetched document retained | the citation is the product and a citation you cannot reopen is not one | | Prices | One provider whose free quota you have read, with an explicit request budget | a daily quota measured in tens of requests changes the whole refresh design | | Filings | The regulator's own full text search and submission feeds, with a declared user agent | the filings are public, free, and the only primary source in the build | | Rules | One plain file of named rules, each producing a hit with its inputs | a score nobody can decompose is a number you will end up trusting | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local interface and the scheduled refresh. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A market data API key whose free allowance you have read** · free tiers exist and are small; paid tiers start where the free allowance stops - Why: The daily allowance decides how many tickers the watchlist can hold, which decides the refresh design. - Get it: Register with a provider, read the current free allowance and the redistribution clause in their terms, and note both with the date. - Verify: One refresh consumes the number of requests you predicted - [ ] **The contact address you will declare to the regulator's service** · free - Why: Automated access is expected to identify itself, and using a fake address is the kind of shortcut that gets an address range blocked. - Get it: Decide which address you are willing to publish in a user agent string. - Verify: The user agent contains a real address and appears in the request log - [ ] **The rules you will actually use, written before any code** · free - Why: Rules chosen after seeing the data are fitted to the data, and a stance built from them means nothing. - Get it: Write each rule as a sentence with its threshold and why that threshold, and date the document. - Verify: Every rule in the code matches a sentence in that document - [ ] **A way to send yourself a notification** · free tiers are ample at this volume - Why: Alerts that only appear in a page you have to open are not alerts. - Get it: Any transactional sender, or a local mail relay you already run. - Verify: A threshold crossing reaches your inbox with the retrieval time in it ## 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: - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. - maintained stock coverage and editorial calls - intraday data quality and provider reliability - managed earnings and price alert delivery - the product's target-price methodology - AI-agent API access If one of those is essential to you, that is the reason to keep paying for StocksBrew, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · StocksBrew 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 personal watchlist that shows the rule hits and the filings behind a stance and never tells you to buy anything to replace StocksBrew. 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 Two constraints decide this build, and both are outside the code. First, do not build something that gives investment advice, and do not build something that looks like it does. Under the United States Investment Advisers Act the definition of an investment adviser at 15 U.S.C. 80b-2(a)(11) excludes, at clause (D), the publisher of a bona fide newspaper, news magazine or business or financial publication of general and regular circulation, and in Lowe v. SEC the Supreme Court read that exclusion as covering impersonal advice that is not tailored to any specific client's portfolio. A private tool you run for yourself is not the problem. Tailored output, shown to other people for money, is a different thing entirely and is where that line sits. So the output of this build is rule hits and cited documents, never a verdict, never a price target, and never a number presented as a forecast. If you cannot point at the filing, do not print the sentence. Second, market data is licensed rather than free. Exchange data policies draw the boundary at a delay: Nasdaq's published policies treat 15 minutes as the default period after which information becomes delayed information, require a signed agreement and monthly reporting from anyone redistributing even that delayed data, and require prior approval before charging for it. Your data provider's terms will say something similar. Everything here therefore stays on your machine, every figure on screen carries its source and the timestamp it was retrieved at, and there is no sharing feature. ### Stack (fixed, do not substitute) - Runtime: Node with a server-rendered interface on the loopback address, plus a scheduled refresh · one reader, no accounts, and nothing that could be mistaken for a publication - Storage: SQLite, one file, with every fetched document retained · the citation is the product and a citation you cannot reopen is not one - Prices: One provider whose free quota you have read, with an explicit request budget · a daily quota measured in tens of requests changes the whole refresh design - Filings: The regulator's own full text search and submission feeds, with a declared user agent · the filings are public, free, and the only primary source in the build - Rules: One plain file of named rules, each producing a hit with its inputs · a score nobody can decompose is a number you will end up trusting ### Data model (create this before Phase 1) - tickers(symbol, name, cik, added_at, active) · the cik is what joins a ticker to its filings - price_snapshots(symbol, as_of_date, close, volume, source, retrieved_at, delayed) · delayed records what the provider said it was giving you - filings(id, cik, form_type, filed_at, accession, url, title, retrieved_at, body_sha256) - news_items(id, symbol, source_url, title, published_at, retrieved_at, dedup_key) - rule_hits(id, symbol, rule_name, as_of_date, inputs_json, fired, explanation) · every rule writes a row whether it fired or not - stances(symbol, as_of_date, stance, hit_ids_json, computed_at) · the stance is a function of hit_ids and nothing else - alerts(id, symbol, kind, threshold_json, last_fired_at, cooldown_minutes, muted) - requests(id, at, provider, endpoint, symbol, status, quota_remaining) · the request budget only works if every call is counted - There is no column called target_price and no column called recommendation. The stance is a label over rule hits, and removing the hits removes the label. ### Phase 1 · A refresh that fits inside the quota Build: Read your provider's current published free allowance yourself before designing the refresh: when checked in September 2026 one widely used provider's free key allowed 25 requests per day, which is tens of symbols, not hundreds. Count every request in a table, cap the watchlist accordingly, and refuse to add a ticker the budget cannot serve rather than degrading everyone's data silently. Trap: Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. Done when: A full refresh of the configured watchlist consumes fewer requests than the configured daily allowance, asserted by counting rows in the requests table. Adding a ticker beyond the budget is refused with the arithmetic shown, tested at the boundary. A symbol that failed to refresh is marked as such and its rules do not run that day, tested by stubbing a failure. Every stored price carries its source, its retrieval time and whether the provider described it as delayed. Do not build yet: No filings, no news, no rules, no alerts. ### Phase 2 · Filings and news that can be reopened Build: Fetch filings from the regulator's own feeds keyed by the company identifier, declaring a real contact in the user agent and staying inside the published request rate, which was 10 requests per second when checked in September 2026. Store the URL, the filing date, the retrieval time and a hash of the body. Deduplicate news by a stable key so the same story from two feeds appears once. Trap: Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. Done when: Every filing row has a URL, a filing date, a retrieval time and a body hash, asserted by a schema test that rejects nulls. The client declares a contact in its user agent and stays inside the published rate, tested with a fake clock. The same news item arriving from two feeds produces one row, tested with a duplicate fixture. A document whose retrieval failed is recorded as a failed attempt rather than omitted silently. Do not build yet: No rules, no stances, no alerts. ### Phase 3 · Rules that show their work Build: One file of named rules, each a pure function over stored data returning fired or not, its inputs and a one-sentence explanation. Write a row for every rule evaluated, including the ones that did not fire. Compose the stance from the hits and store the hit identifiers with it. Refuse to show a stance when any input for that symbol is stale beyond a configured age. Trap: Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. Done when: Every rule evaluation writes a row including non-firing rules, checked in the database after a refresh. A stance displays with the full list of hits that produced it and cannot render without them, asserted in the suite. Changing one rule's threshold changes exactly the stances whose hits involved that rule, tested with a fixture. A symbol with stale inputs shows the staleness instead of a stance, tested at the boundary. Do not build yet: No alerts, no export, no operational documentation. ### Phase 4 · Alerts that fire once and say how old the data is Build: Per-ticker price thresholds and an upcoming filing or earnings reminder. Fire on a crossing rather than on a level, so a price sitting at the threshold does not notify every refresh. Include the retrieval time and whether the data was delayed in the message body. A cooldown per alert, and a mute that is a stored state rather than a deleted row. Trap: Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. Done when: A price sitting past its threshold across several refreshes produces one notification, tested with a repeated fixture. Every notification body contains the retrieval time and the delayed flag of the data it was computed from, asserted in the suite. Muting an alert preserves its configuration and history, and unmuting does not fire retrospectively. A notification send failure is retried and then surfaced in the interface rather than dropped. Do not build yet: No export, no retention, no disclaimer work yet. ### Phase 5 · Staleness, export and the line you do not cross Build: A staleness badge on every figure computed from its retrieval time. Export tickers, prices, filings, hits, stances and alerts as CSV, plus the stored documents. No sharing feature, no public page, no syndication route, and a disclaimer on every page rather than only in the README. Track the provider request count against the allowance on the same page. Trap: Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. Done when: Every displayed figure carries a staleness indicator derived from its retrieval time, checked across the watchlist and detail views. The application binds to the loopback address only and refuses to start otherwise with an explanation, tested. Export produces every table plus the stored documents and reimports into an empty database with equal counts. The disclaimer is rendered by the layout, so no page can be added without it, asserted in the suite. Do not build yet: Nothing after this. ### Out of scope (and why) - Price targets, forecasts and anything resembling a recommendation, for the reasons in rule zero and because the build has no basis for any of them. - Intraday and real-time quotes, which are licensed products with agreements attached rather than a different endpoint on the same key. - Brokerage connections and order placement, which turn a reading tool into something that can lose money by accident. - Sharing, publishing or syndicating the output, which is both a redistribution question about the data and the line between a private tool and a publication. ### README must contain - That the output is rule hits and cited documents, never advice, with the disclaimer reproduced in full - That market data is licensed, that the tool is local only, and that there is deliberately no sharing route - The provider's free allowance you designed around, the date you checked it, and how many tickers it supports - That filings are fetched with a declared contact inside the published request rate, with that rate and its date - How staleness is computed and why a stale input suppresses a stance rather than dimming it ===== AGENTS.md ===== # Agent instructions · StocksBrew indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node with a server-rendered interface on the loopback address, plus a scheduled refresh, SQLite, one file, with every fetched document retained, One provider whose free quota you have read, with an explicit request budget, The regulator's own full text search and submission feeds, with a declared user agent, One plain file of named rules, each producing a hit with its inputs. 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 - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ===== BUILD_PLAN.md ===== # Build plan · StocksBrew A personal research cockpit whose hard problems are producing a stance that is nothing more than a visible sum of stated rules, keeping every displayed number tied to a source and a retrieval time, and fitting a daily refresh inside a data quota that is far smaller than the watchlist wants to be. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A refresh that fits inside the quota A daily update of the whole watchlist stays inside the provider's published free allowance, with every call counted. ### Steps 1. Read the provider's published allowance and write it down The refresh design follows from this number, not the other way round. Files: `config/providers.json`, `docs/quota.md` 2. Count every request in a table before making it A budget you cannot audit is not a budget. Files: `src/data/budget.js`, `tests/budget.test.js` 3. Refuse a ticker the budget cannot serve Silent degradation is worse than a clear refusal. Files: `src/watchlist/add.js` ### Done when - [ ] A full refresh stays inside the daily allowance - [ ] An unaffordable ticker is refused with the arithmetic - [ ] Failed symbols are marked and skipped by rules - [ ] Every price carries source, time and delay status ### Watch out - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. ## Phase 2 · Filings and news that can be reopened Every document the tool mentions is stored with its URL, its retrieval time and a hash of what was retrieved. ### Steps 1. Declare who you are and respect the published rate The regulator publishes both and expects both. Files: `src/data/edgar.js` 2. Store the URL, the time and the hash A citation you cannot reopen is a claim. Files: `src/data/filings.js`, `tests/filings.test.js` 3. Deduplicate news on a stable key The same story from three feeds is one event. Files: `src/data/news.js` ### Done when - [ ] Filings carry URL, date, retrieval time and hash - [ ] The client declares a contact and honours the rate - [ ] Duplicate news items collapse to one row - [ ] Failed retrievals are recorded, not omitted ### Watch out - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. ## Phase 3 · Rules that show their work A stance is a visible function of named rule hits, and no hits means no stance. ### Steps 1. Keep every rule pure and named in one file You will read this file more than any other. Files: `src/rules/rules.js`, `tests/rules.test.js` 2. Record non-firing rules too What did not fire is half the explanation. Files: `src/rules/evaluate.js` 3. Make a stale input suppress the stance A stance computed from Tuesday's price is fiction. Files: `src/rules/staleness.js` ### Done when - [ ] Every evaluated rule writes a row - [ ] A stance cannot render without its hits - [ ] A threshold change moves only the related stances - [ ] Stale inputs suppress the stance ### Watch out - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. ## Phase 4 · Alerts that fire once and say how old the data is A threshold crossing notifies you exactly once, with the retrieval time in the message. ### Steps 1. Fire on a crossing, not on a level A standing condition is not news after the first time. Files: `src/alerts/crossing.js`, `tests/crossing.test.js` 2. Put the retrieval time in the message Acting on a figure means knowing how old it is. Files: `src/alerts/message.js` 3. Mute as a state, never as a deletion You will want the history back later. Files: `src/alerts/mute.js` ### Done when - [ ] A standing condition notifies once - [ ] Messages carry retrieval time and delay status - [ ] Muting preserves configuration and history - [ ] Send failures are retried and surfaced ### Watch out - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ## Not in this build - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. ## After v1, if you want it - A rule backtest over your own stored history, reported with its sample size, which is the only honest way to judge a rule - A quarterly review that lists the rules that never fired, which is usually most of them - A second price source used only to cross-check the first, which catches a bad print faster than any rule does ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Prices, filings, news, hits and stances. One file to copy. DATABASE_PATH=./data/research.db # Required · secret. Market data provider. Its allowance is the constraint the refresh is built around. PRICE_API_KEY=redacted # Required. The provider's published free allowance. Recheck it and update this. DAILY_REQUEST_ALLOWANCE=25 # Required. Declared on every filing request, as the service asks. EDGAR_USER_AGENT=Your Name your.address@example.com # Required. Beyond this an input is stale and its symbol shows no stance. STALE_AFTER_HOURS=30 # Required · secret. Where alerts go. Nothing else leaves the machine. SMTP_URL=smtps://user:redacted@mail.example.com:465 # Required. Loopback only. The server refuses to start on anything else. BIND_ADDRESS=127.0.0.1
You are building a lean indie version of StocksBrew. 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 ===== # StocksBrew · indie build A personal research cockpit whose hard problems are producing a stance that is nothing more than a visible sum of stated rules, keeping every displayed number tied to a source and a retrieval time, and fitting a daily refresh inside a data quota that is far smaller than the watchlist wants to be. Estimated effort: **multi-day**. 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 | Node with a server-rendered interface on the loopback address, plus a scheduled refresh | one reader, no accounts, and nothing that could be mistaken for a publication | | Storage | SQLite, one file, with every fetched document retained | the citation is the product and a citation you cannot reopen is not one | | Prices | One provider whose free quota you have read, with an explicit request budget | a daily quota measured in tens of requests changes the whole refresh design | | Filings | The regulator's own full text search and submission feeds, with a declared user agent | the filings are public, free, and the only primary source in the build | | Rules | One plain file of named rules, each producing a hit with its inputs | a score nobody can decompose is a number you will end up trusting | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local interface and the scheduled refresh. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A market data API key whose free allowance you have read** · free tiers exist and are small; paid tiers start where the free allowance stops - Why: The daily allowance decides how many tickers the watchlist can hold, which decides the refresh design. - Get it: Register with a provider, read the current free allowance and the redistribution clause in their terms, and note both with the date. - Verify: One refresh consumes the number of requests you predicted - [ ] **The contact address you will declare to the regulator's service** · free - Why: Automated access is expected to identify itself, and using a fake address is the kind of shortcut that gets an address range blocked. - Get it: Decide which address you are willing to publish in a user agent string. - Verify: The user agent contains a real address and appears in the request log - [ ] **The rules you will actually use, written before any code** · free - Why: Rules chosen after seeing the data are fitted to the data, and a stance built from them means nothing. - Get it: Write each rule as a sentence with its threshold and why that threshold, and date the document. - Verify: Every rule in the code matches a sentence in that document - [ ] **A way to send yourself a notification** · free tiers are ample at this volume - Why: Alerts that only appear in a page you have to open are not alerts. - Get it: Any transactional sender, or a local mail relay you already run. - Verify: A threshold crossing reaches your inbox with the retrieval time in it ## 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: - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. - maintained stock coverage and editorial calls - intraday data quality and provider reliability - managed earnings and price alert delivery - the product's target-price methodology - AI-agent API access If one of those is essential to you, that is the reason to keep paying for StocksBrew, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · StocksBrew 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 personal watchlist that shows the rule hits and the filings behind a stance and never tells you to buy anything to replace StocksBrew. 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 Two constraints decide this build, and both are outside the code. First, do not build something that gives investment advice, and do not build something that looks like it does. Under the United States Investment Advisers Act the definition of an investment adviser at 15 U.S.C. 80b-2(a)(11) excludes, at clause (D), the publisher of a bona fide newspaper, news magazine or business or financial publication of general and regular circulation, and in Lowe v. SEC the Supreme Court read that exclusion as covering impersonal advice that is not tailored to any specific client's portfolio. A private tool you run for yourself is not the problem. Tailored output, shown to other people for money, is a different thing entirely and is where that line sits. So the output of this build is rule hits and cited documents, never a verdict, never a price target, and never a number presented as a forecast. If you cannot point at the filing, do not print the sentence. Second, market data is licensed rather than free. Exchange data policies draw the boundary at a delay: Nasdaq's published policies treat 15 minutes as the default period after which information becomes delayed information, require a signed agreement and monthly reporting from anyone redistributing even that delayed data, and require prior approval before charging for it. Your data provider's terms will say something similar. Everything here therefore stays on your machine, every figure on screen carries its source and the timestamp it was retrieved at, and there is no sharing feature. ### Stack (fixed, do not substitute) - Runtime: Node with a server-rendered interface on the loopback address, plus a scheduled refresh · one reader, no accounts, and nothing that could be mistaken for a publication - Storage: SQLite, one file, with every fetched document retained · the citation is the product and a citation you cannot reopen is not one - Prices: One provider whose free quota you have read, with an explicit request budget · a daily quota measured in tens of requests changes the whole refresh design - Filings: The regulator's own full text search and submission feeds, with a declared user agent · the filings are public, free, and the only primary source in the build - Rules: One plain file of named rules, each producing a hit with its inputs · a score nobody can decompose is a number you will end up trusting ### Data model (create this before Phase 1) - tickers(symbol, name, cik, added_at, active) · the cik is what joins a ticker to its filings - price_snapshots(symbol, as_of_date, close, volume, source, retrieved_at, delayed) · delayed records what the provider said it was giving you - filings(id, cik, form_type, filed_at, accession, url, title, retrieved_at, body_sha256) - news_items(id, symbol, source_url, title, published_at, retrieved_at, dedup_key) - rule_hits(id, symbol, rule_name, as_of_date, inputs_json, fired, explanation) · every rule writes a row whether it fired or not - stances(symbol, as_of_date, stance, hit_ids_json, computed_at) · the stance is a function of hit_ids and nothing else - alerts(id, symbol, kind, threshold_json, last_fired_at, cooldown_minutes, muted) - requests(id, at, provider, endpoint, symbol, status, quota_remaining) · the request budget only works if every call is counted - There is no column called target_price and no column called recommendation. The stance is a label over rule hits, and removing the hits removes the label. ### Phase 1 · A refresh that fits inside the quota Build: Read your provider's current published free allowance yourself before designing the refresh: when checked in September 2026 one widely used provider's free key allowed 25 requests per day, which is tens of symbols, not hundreds. Count every request in a table, cap the watchlist accordingly, and refuse to add a ticker the budget cannot serve rather than degrading everyone's data silently. Trap: Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. Done when: A full refresh of the configured watchlist consumes fewer requests than the configured daily allowance, asserted by counting rows in the requests table. Adding a ticker beyond the budget is refused with the arithmetic shown, tested at the boundary. A symbol that failed to refresh is marked as such and its rules do not run that day, tested by stubbing a failure. Every stored price carries its source, its retrieval time and whether the provider described it as delayed. Do not build yet: No filings, no news, no rules, no alerts. ### Phase 2 · Filings and news that can be reopened Build: Fetch filings from the regulator's own feeds keyed by the company identifier, declaring a real contact in the user agent and staying inside the published request rate, which was 10 requests per second when checked in September 2026. Store the URL, the filing date, the retrieval time and a hash of the body. Deduplicate news by a stable key so the same story from two feeds appears once. Trap: Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. Done when: Every filing row has a URL, a filing date, a retrieval time and a body hash, asserted by a schema test that rejects nulls. The client declares a contact in its user agent and stays inside the published rate, tested with a fake clock. The same news item arriving from two feeds produces one row, tested with a duplicate fixture. A document whose retrieval failed is recorded as a failed attempt rather than omitted silently. Do not build yet: No rules, no stances, no alerts. ### Phase 3 · Rules that show their work Build: One file of named rules, each a pure function over stored data returning fired or not, its inputs and a one-sentence explanation. Write a row for every rule evaluated, including the ones that did not fire. Compose the stance from the hits and store the hit identifiers with it. Refuse to show a stance when any input for that symbol is stale beyond a configured age. Trap: Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. Done when: Every rule evaluation writes a row including non-firing rules, checked in the database after a refresh. A stance displays with the full list of hits that produced it and cannot render without them, asserted in the suite. Changing one rule's threshold changes exactly the stances whose hits involved that rule, tested with a fixture. A symbol with stale inputs shows the staleness instead of a stance, tested at the boundary. Do not build yet: No alerts, no export, no operational documentation. ### Phase 4 · Alerts that fire once and say how old the data is Build: Per-ticker price thresholds and an upcoming filing or earnings reminder. Fire on a crossing rather than on a level, so a price sitting at the threshold does not notify every refresh. Include the retrieval time and whether the data was delayed in the message body. A cooldown per alert, and a mute that is a stored state rather than a deleted row. Trap: Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. Done when: A price sitting past its threshold across several refreshes produces one notification, tested with a repeated fixture. Every notification body contains the retrieval time and the delayed flag of the data it was computed from, asserted in the suite. Muting an alert preserves its configuration and history, and unmuting does not fire retrospectively. A notification send failure is retried and then surfaced in the interface rather than dropped. Do not build yet: No export, no retention, no disclaimer work yet. ### Phase 5 · Staleness, export and the line you do not cross Build: A staleness badge on every figure computed from its retrieval time. Export tickers, prices, filings, hits, stances and alerts as CSV, plus the stored documents. No sharing feature, no public page, no syndication route, and a disclaimer on every page rather than only in the README. Track the provider request count against the allowance on the same page. Trap: Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. Done when: Every displayed figure carries a staleness indicator derived from its retrieval time, checked across the watchlist and detail views. The application binds to the loopback address only and refuses to start otherwise with an explanation, tested. Export produces every table plus the stored documents and reimports into an empty database with equal counts. The disclaimer is rendered by the layout, so no page can be added without it, asserted in the suite. Do not build yet: Nothing after this. ### Out of scope (and why) - Price targets, forecasts and anything resembling a recommendation, for the reasons in rule zero and because the build has no basis for any of them. - Intraday and real-time quotes, which are licensed products with agreements attached rather than a different endpoint on the same key. - Brokerage connections and order placement, which turn a reading tool into something that can lose money by accident. - Sharing, publishing or syndicating the output, which is both a redistribution question about the data and the line between a private tool and a publication. ### README must contain - That the output is rule hits and cited documents, never advice, with the disclaimer reproduced in full - That market data is licensed, that the tool is local only, and that there is deliberately no sharing route - The provider's free allowance you designed around, the date you checked it, and how many tickers it supports - That filings are fetched with a declared contact inside the published request rate, with that rate and its date - How staleness is computed and why a stale input suppresses a stance rather than dimming it ===== AGENTS.md ===== # Agent instructions · StocksBrew indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node with a server-rendered interface on the loopback address, plus a scheduled refresh, SQLite, one file, with every fetched document retained, One provider whose free quota you have read, with an explicit request budget, The regulator's own full text search and submission feeds, with a declared user agent, One plain file of named rules, each producing a hit with its inputs. 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 - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ===== BUILD_PLAN.md ===== # Build plan · StocksBrew A personal research cockpit whose hard problems are producing a stance that is nothing more than a visible sum of stated rules, keeping every displayed number tied to a source and a retrieval time, and fitting a daily refresh inside a data quota that is far smaller than the watchlist wants to be. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A refresh that fits inside the quota A daily update of the whole watchlist stays inside the provider's published free allowance, with every call counted. ### Steps 1. Read the provider's published allowance and write it down The refresh design follows from this number, not the other way round. Files: `config/providers.json`, `docs/quota.md` 2. Count every request in a table before making it A budget you cannot audit is not a budget. Files: `src/data/budget.js`, `tests/budget.test.js` 3. Refuse a ticker the budget cannot serve Silent degradation is worse than a clear refusal. Files: `src/watchlist/add.js` ### Done when - [ ] A full refresh stays inside the daily allowance - [ ] An unaffordable ticker is refused with the arithmetic - [ ] Failed symbols are marked and skipped by rules - [ ] Every price carries source, time and delay status ### Watch out - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. ## Phase 2 · Filings and news that can be reopened Every document the tool mentions is stored with its URL, its retrieval time and a hash of what was retrieved. ### Steps 1. Declare who you are and respect the published rate The regulator publishes both and expects both. Files: `src/data/edgar.js` 2. Store the URL, the time and the hash A citation you cannot reopen is a claim. Files: `src/data/filings.js`, `tests/filings.test.js` 3. Deduplicate news on a stable key The same story from three feeds is one event. Files: `src/data/news.js` ### Done when - [ ] Filings carry URL, date, retrieval time and hash - [ ] The client declares a contact and honours the rate - [ ] Duplicate news items collapse to one row - [ ] Failed retrievals are recorded, not omitted ### Watch out - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. ## Phase 3 · Rules that show their work A stance is a visible function of named rule hits, and no hits means no stance. ### Steps 1. Keep every rule pure and named in one file You will read this file more than any other. Files: `src/rules/rules.js`, `tests/rules.test.js` 2. Record non-firing rules too What did not fire is half the explanation. Files: `src/rules/evaluate.js` 3. Make a stale input suppress the stance A stance computed from Tuesday's price is fiction. Files: `src/rules/staleness.js` ### Done when - [ ] Every evaluated rule writes a row - [ ] A stance cannot render without its hits - [ ] A threshold change moves only the related stances - [ ] Stale inputs suppress the stance ### Watch out - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. ## Phase 4 · Alerts that fire once and say how old the data is A threshold crossing notifies you exactly once, with the retrieval time in the message. ### Steps 1. Fire on a crossing, not on a level A standing condition is not news after the first time. Files: `src/alerts/crossing.js`, `tests/crossing.test.js` 2. Put the retrieval time in the message Acting on a figure means knowing how old it is. Files: `src/alerts/message.js` 3. Mute as a state, never as a deletion You will want the history back later. Files: `src/alerts/mute.js` ### Done when - [ ] A standing condition notifies once - [ ] Messages carry retrieval time and delay status - [ ] Muting preserves configuration and history - [ ] Send failures are retried and surfaced ### Watch out - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ## Not in this build - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. ## After v1, if you want it - A rule backtest over your own stored history, reported with its sample size, which is the only honest way to judge a rule - A quarterly review that lists the rules that never fired, which is usually most of them - A second price source used only to cross-check the first, which catches a bad print faster than any rule does ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Prices, filings, news, hits and stances. One file to copy. DATABASE_PATH=./data/research.db # Required · secret. Market data provider. Its allowance is the constraint the refresh is built around. PRICE_API_KEY=redacted # Required. The provider's published free allowance. Recheck it and update this. DAILY_REQUEST_ALLOWANCE=25 # Required. Declared on every filing request, as the service asks. EDGAR_USER_AGENT=Your Name your.address@example.com # Required. Beyond this an input is stale and its symbol shows no stance. STALE_AFTER_HOURS=30 # Required · secret. Where alerts go. Nothing else leaves the machine. SMTP_URL=smtps://user:redacted@mail.example.com:465 # Required. Loopback only. The server refuses to start on anything else. BIND_ADDRESS=127.0.0.1
You are building a production product version of StocksBrew. 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 ===== # StocksBrew · product brief ## Problem A personal stock-research cockpit is very buildable, but StocksBrew's useful current calls depend on continuously maintained market, filings, news, and alert pipelines. A DIY version can make its evidence and rules explicit; it will not inherit the service's coverage, refresh reliability, or editorial judgment. ## Product outcome A private cockpit whose every number carries a source and an age, whose stance is a visible sum of named rules, and which has no route by which it could become a publication or a redistribution of licensed data. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - market-data API key - SEC EDGAR data - RSS news sources - LLM API key - scheduled job ## Explicit non-goals for v1 - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. - maintained stock coverage and editorial calls - intraday data quality and provider reliability - managed earnings and price alert delivery - the product's target-price methodology - AI-agent API access ## Success criteria - A full refresh is confirmed to stay inside the daily allowance - Every filing is confirmed to carry URL, retrieval time and hash - A stance is confirmed unable to render without its hits - A standing threshold is confirmed to notify once - The server is confirmed to refuse a non-loopback bind ===== BRIEF.md ===== # Build brief · StocksBrew 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 personal watchlist that shows the rule hits and the filings behind a stance and never tells you to buy anything to replace StocksBrew. 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 Two constraints decide this build, and both are outside the code. First, do not build something that gives investment advice, and do not build something that looks like it does. Under the United States Investment Advisers Act the definition of an investment adviser at 15 U.S.C. 80b-2(a)(11) excludes, at clause (D), the publisher of a bona fide newspaper, news magazine or business or financial publication of general and regular circulation, and in Lowe v. SEC the Supreme Court read that exclusion as covering impersonal advice that is not tailored to any specific client's portfolio. A private tool you run for yourself is not the problem. Tailored output, shown to other people for money, is a different thing entirely and is where that line sits. So the output of this build is rule hits and cited documents, never a verdict, never a price target, and never a number presented as a forecast. If you cannot point at the filing, do not print the sentence. Second, market data is licensed rather than free. Exchange data policies draw the boundary at a delay: Nasdaq's published policies treat 15 minutes as the default period after which information becomes delayed information, require a signed agreement and monthly reporting from anyone redistributing even that delayed data, and require prior approval before charging for it. Your data provider's terms will say something similar. Everything here therefore stays on your machine, every figure on screen carries its source and the timestamp it was retrieved at, and there is no sharing feature. ### Stack (fixed, do not substitute) - Runtime: Node with a server-rendered interface on the loopback address, plus a scheduled refresh · one reader, no accounts, and nothing that could be mistaken for a publication - Storage: SQLite, one file, with every fetched document retained · the citation is the product and a citation you cannot reopen is not one - Prices: One provider whose free quota you have read, with an explicit request budget · a daily quota measured in tens of requests changes the whole refresh design - Filings: The regulator's own full text search and submission feeds, with a declared user agent · the filings are public, free, and the only primary source in the build - Rules: One plain file of named rules, each producing a hit with its inputs · a score nobody can decompose is a number you will end up trusting ### Data model (create this before Phase 1) - tickers(symbol, name, cik, added_at, active) · the cik is what joins a ticker to its filings - price_snapshots(symbol, as_of_date, close, volume, source, retrieved_at, delayed) · delayed records what the provider said it was giving you - filings(id, cik, form_type, filed_at, accession, url, title, retrieved_at, body_sha256) - news_items(id, symbol, source_url, title, published_at, retrieved_at, dedup_key) - rule_hits(id, symbol, rule_name, as_of_date, inputs_json, fired, explanation) · every rule writes a row whether it fired or not - stances(symbol, as_of_date, stance, hit_ids_json, computed_at) · the stance is a function of hit_ids and nothing else - alerts(id, symbol, kind, threshold_json, last_fired_at, cooldown_minutes, muted) - requests(id, at, provider, endpoint, symbol, status, quota_remaining) · the request budget only works if every call is counted - There is no column called target_price and no column called recommendation. The stance is a label over rule hits, and removing the hits removes the label. ### Phase 1 · A refresh that fits inside the quota Build: Read your provider's current published free allowance yourself before designing the refresh: when checked in September 2026 one widely used provider's free key allowed 25 requests per day, which is tens of symbols, not hundreds. Count every request in a table, cap the watchlist accordingly, and refuse to add a ticker the budget cannot serve rather than degrading everyone's data silently. Trap: Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. Done when: A full refresh of the configured watchlist consumes fewer requests than the configured daily allowance, asserted by counting rows in the requests table. Adding a ticker beyond the budget is refused with the arithmetic shown, tested at the boundary. A symbol that failed to refresh is marked as such and its rules do not run that day, tested by stubbing a failure. Every stored price carries its source, its retrieval time and whether the provider described it as delayed. Do not build yet: No filings, no news, no rules, no alerts. ### Phase 2 · Filings and news that can be reopened Build: Fetch filings from the regulator's own feeds keyed by the company identifier, declaring a real contact in the user agent and staying inside the published request rate, which was 10 requests per second when checked in September 2026. Store the URL, the filing date, the retrieval time and a hash of the body. Deduplicate news by a stable key so the same story from two feeds appears once. Trap: Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. Done when: Every filing row has a URL, a filing date, a retrieval time and a body hash, asserted by a schema test that rejects nulls. The client declares a contact in its user agent and stays inside the published rate, tested with a fake clock. The same news item arriving from two feeds produces one row, tested with a duplicate fixture. A document whose retrieval failed is recorded as a failed attempt rather than omitted silently. Do not build yet: No rules, no stances, no alerts. ### Phase 3 · Rules that show their work Build: One file of named rules, each a pure function over stored data returning fired or not, its inputs and a one-sentence explanation. Write a row for every rule evaluated, including the ones that did not fire. Compose the stance from the hits and store the hit identifiers with it. Refuse to show a stance when any input for that symbol is stale beyond a configured age. Trap: Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. Done when: Every rule evaluation writes a row including non-firing rules, checked in the database after a refresh. A stance displays with the full list of hits that produced it and cannot render without them, asserted in the suite. Changing one rule's threshold changes exactly the stances whose hits involved that rule, tested with a fixture. A symbol with stale inputs shows the staleness instead of a stance, tested at the boundary. Do not build yet: No alerts, no export, no operational documentation. ### Phase 4 · Alerts that fire once and say how old the data is Build: Per-ticker price thresholds and an upcoming filing or earnings reminder. Fire on a crossing rather than on a level, so a price sitting at the threshold does not notify every refresh. Include the retrieval time and whether the data was delayed in the message body. A cooldown per alert, and a mute that is a stored state rather than a deleted row. Trap: Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. Done when: A price sitting past its threshold across several refreshes produces one notification, tested with a repeated fixture. Every notification body contains the retrieval time and the delayed flag of the data it was computed from, asserted in the suite. Muting an alert preserves its configuration and history, and unmuting does not fire retrospectively. A notification send failure is retried and then surfaced in the interface rather than dropped. Do not build yet: No export, no retention, no disclaimer work yet. ### Phase 5 · Staleness, export and the line you do not cross Build: A staleness badge on every figure computed from its retrieval time. Export tickers, prices, filings, hits, stances and alerts as CSV, plus the stored documents. No sharing feature, no public page, no syndication route, and a disclaimer on every page rather than only in the README. Track the provider request count against the allowance on the same page. Trap: Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. Done when: Every displayed figure carries a staleness indicator derived from its retrieval time, checked across the watchlist and detail views. The application binds to the loopback address only and refuses to start otherwise with an explanation, tested. Export produces every table plus the stored documents and reimports into an empty database with equal counts. The disclaimer is rendered by the layout, so no page can be added without it, asserted in the suite. Do not build yet: Nothing after this. ### Out of scope (and why) - Price targets, forecasts and anything resembling a recommendation, for the reasons in rule zero and because the build has no basis for any of them. - Intraday and real-time quotes, which are licensed products with agreements attached rather than a different endpoint on the same key. - Brokerage connections and order placement, which turn a reading tool into something that can lose money by accident. - Sharing, publishing or syndicating the output, which is both a redistribution question about the data and the line between a private tool and a publication. ### README must contain - That the output is rule hits and cited documents, never advice, with the disclaimer reproduced in full - That market data is licensed, that the tool is local only, and that there is deliberately no sharing route - The provider's free allowance you designed around, the date you checked it, and how many tickers it supports - That filings are fetched with a declared contact inside the published request rate, with that rate and its date - How staleness is computed and why a stale input suppresses a stance rather than dimming it ===== ARCHITECTURE.md ===== # Architecture · StocksBrew ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a server-rendered interface on the loopback address, plus a scheduled refresh | one reader, no accounts, and nothing that could be mistaken for a publication | | Storage | SQLite, one file, with every fetched document retained | the citation is the product and a citation you cannot reopen is not one | | Prices | One provider whose free quota you have read, with an explicit request budget | a daily quota measured in tens of requests changes the whole refresh design | | Filings | The regulator's own full text search and submission feeds, with a declared user agent | the filings are public, free, and the only primary source in the build | | Rules | One plain file of named rules, each producing a hit with its inputs | a score nobody can decompose is a number you will end up trusting | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Budget | Request counting, allowance enforcement and refusals. | Any provider, provided the allowance is read and the counting stays. | | Sources | Filings, news, retrieval times and body hashes. | Any feed, provided documents remain reopenable. | | Rules | Named pure rules and hit rows for every evaluation. | Any rule set, provided non-firing rules are recorded too. | | Stance | Composition from hits and staleness suppression. | Not swappable for a weighted score without the hits beside it. | | Alerts | Crossings, cooldowns and messages carrying data age. | Any channel, provided the message states how old the data was. | ## 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 · Prices, filings, news, hits and stances. One file to copy. - `PRICE_API_KEY` · required, secret · Market data provider. Its allowance is the constraint the refresh is built around. - `DAILY_REQUEST_ALLOWANCE` · required · The provider's published free allowance. Recheck it and update this. - `EDGAR_USER_AGENT` · required · Declared on every filing request, as the service asks. - `STALE_AFTER_HOURS` · required · Beyond this an input is stale and its symbol shows no stance. - `SMTP_URL` · required, secret · Where alerts go. Nothing else leaves the machine. - `BIND_ADDRESS` · required · Loopback only. The server refuses to start on anything else. ## 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 · StocksBrew product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node with a server-rendered interface on the loopback address, plus a scheduled refresh, SQLite, one file, with every fetched document retained, One provider whose free quota you have read, with an explicit request budget, The regulator's own full text search and submission feeds, with a declared user agent, One plain file of named rules, each producing a hit with its inputs. - 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 - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. - Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. ===== MILESTONES.md ===== # Delivery milestones · StocksBrew Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A refresh that fits inside the quota A daily update of the whole watchlist stays inside the provider's published free allowance, with every call counted. ### Steps 1. Read the provider's published allowance and write it down The refresh design follows from this number, not the other way round. Files: `config/providers.json`, `docs/quota.md` 2. Count every request in a table before making it A budget you cannot audit is not a budget. Files: `src/data/budget.js`, `tests/budget.test.js` 3. Refuse a ticker the budget cannot serve Silent degradation is worse than a clear refusal. Files: `src/watchlist/add.js` ### Done when - [ ] A full refresh stays inside the daily allowance - [ ] An unaffordable ticker is refused with the arithmetic - [ ] Failed symbols are marked and skipped by rules - [ ] Every price carries source, time and delay status ### Watch out - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. ## M2 · Filings and news that can be reopened Every document the tool mentions is stored with its URL, its retrieval time and a hash of what was retrieved. ### Steps 1. Declare who you are and respect the published rate The regulator publishes both and expects both. Files: `src/data/edgar.js` 2. Store the URL, the time and the hash A citation you cannot reopen is a claim. Files: `src/data/filings.js`, `tests/filings.test.js` 3. Deduplicate news on a stable key The same story from three feeds is one event. Files: `src/data/news.js` ### Done when - [ ] Filings carry URL, date, retrieval time and hash - [ ] The client declares a contact and honours the rate - [ ] Duplicate news items collapse to one row - [ ] Failed retrievals are recorded, not omitted ### Watch out - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. ## M3 · Rules that show their work A stance is a visible function of named rule hits, and no hits means no stance. ### Steps 1. Keep every rule pure and named in one file You will read this file more than any other. Files: `src/rules/rules.js`, `tests/rules.test.js` 2. Record non-firing rules too What did not fire is half the explanation. Files: `src/rules/evaluate.js` 3. Make a stale input suppress the stance A stance computed from Tuesday's price is fiction. Files: `src/rules/staleness.js` ### Done when - [ ] Every evaluated rule writes a row - [ ] A stance cannot render without its hits - [ ] A threshold change moves only the related stances - [ ] Stale inputs suppress the stance ### Watch out - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. ## M4 · Alerts that fire once and say how old the data is A threshold crossing notifies you exactly once, with the retrieval time in the message. ### Steps 1. Fire on a crossing, not on a level A standing condition is not news after the first time. Files: `src/alerts/crossing.js`, `tests/crossing.test.js` 2. Put the retrieval time in the message Acting on a figure means knowing how old it is. Files: `src/alerts/message.js` 3. Mute as a state, never as a deletion You will want the history back later. Files: `src/alerts/mute.js` ### Done when - [ ] A standing condition notifies once - [ ] Messages carry retrieval time and delay status - [ ] Muting preserves configuration and history - [ ] Send failures are retried and surfaced ### Watch out - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ## M5 · Staleness, export and the line you do not cross (production only) Old data is visibly old, everything exports, and the build refuses to become a publication. ### Steps 1. Derive staleness from retrieval time everywhere The age of a number is part of the number. Files: `src/web/staleness.js` 2. Bind to loopback and refuse anything else This is the technical form of rule zero. Files: `src/server.js`, `tests/loopback-only.test.js` 3. Put the disclaimer in the layout, not the README A page added next month should inherit it. Files: `src/web/layout.js` ### Done when - [ ] Every figure shows its staleness - [ ] The server refuses a non-loopback bind - [ ] Export and import round-trip tables and documents - [ ] The disclaimer is inherited by every page ### Watch out - Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. ===== OPERATIONS.md ===== # Operations · StocksBrew ## Backup The SQLite file and the stored documents together. The prices can be refetched within the allowance; the filings you already retrieved and hashed are the part that makes an old stance explainable. ## Restore Restore, run one refresh, and confirm that request counts match the expectation and that stances recompute to the same labels from the same hit identifiers. A different stance from the same hits means a rule changed and was not recorded. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Requests used against the allowance, the oldest retrieval time in the watchlist, the count of symbols suppressed for staleness, and notification failures. The staleness count is the one that reveals a quota you have quietly outgrown. ## Incident checklist The incident here is not an outage, it is a decision taken on a figure that was older or more delayed than it appeared. Retrieval times on every row and the delayed flag from the provider are what let you reconstruct what the screen actually said that morning. 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 - [ ] A full refresh is confirmed to stay inside the daily allowance - [ ] Every filing is confirmed to carry URL, retrieval time and hash - [ ] A stance is confirmed unable to render without its hits - [ ] A standing threshold is confirmed to notify once - [ ] The server is confirmed to refuse a non-loopback bind ## Launch constraint Do not market omitted StocksBrew 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. Prices, filings, news, hits and stances. One file to copy. DATABASE_PATH=./data/research.db # Required · secret. Market data provider. Its allowance is the constraint the refresh is built around. PRICE_API_KEY=redacted # Required. The provider's published free allowance. Recheck it and update this. DAILY_REQUEST_ALLOWANCE=25 # Required. Declared on every filing request, as the service asks. EDGAR_USER_AGENT=Your Name your.address@example.com # Required. Beyond this an input is stale and its symbol shows no stance. STALE_AFTER_HOURS=30 # Required · secret. Where alerts go. Nothing else leaves the machine. SMTP_URL=smtps://user:redacted@mail.example.com:465 # Required. Loopback only. The server refuses to start on anything else. BIND_ADDRESS=127.0.0.1
# StocksBrew · indie build A personal research cockpit whose hard problems are producing a stance that is nothing more than a visible sum of stated rules, keeping every displayed number tied to a source and a retrieval time, and fitting a daily refresh inside a data quota that is far smaller than the watchlist wants to be. Estimated effort: **multi-day**. 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 | Node with a server-rendered interface on the loopback address, plus a scheduled refresh | one reader, no accounts, and nothing that could be mistaken for a publication | | Storage | SQLite, one file, with every fetched document retained | the citation is the product and a citation you cannot reopen is not one | | Prices | One provider whose free quota you have read, with an explicit request budget | a daily quota measured in tens of requests changes the whole refresh design | | Filings | The regulator's own full text search and submission feeds, with a declared user agent | the filings are public, free, and the only primary source in the build | | Rules | One plain file of named rules, each producing a hit with its inputs | a score nobody can decompose is a number you will end up trusting | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local interface and the scheduled refresh. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A market data API key whose free allowance you have read** · free tiers exist and are small; paid tiers start where the free allowance stops - Why: The daily allowance decides how many tickers the watchlist can hold, which decides the refresh design. - Get it: Register with a provider, read the current free allowance and the redistribution clause in their terms, and note both with the date. - Verify: One refresh consumes the number of requests you predicted - [ ] **The contact address you will declare to the regulator's service** · free - Why: Automated access is expected to identify itself, and using a fake address is the kind of shortcut that gets an address range blocked. - Get it: Decide which address you are willing to publish in a user agent string. - Verify: The user agent contains a real address and appears in the request log - [ ] **The rules you will actually use, written before any code** · free - Why: Rules chosen after seeing the data are fitted to the data, and a stance built from them means nothing. - Get it: Write each rule as a sentence with its threshold and why that threshold, and date the document. - Verify: Every rule in the code matches a sentence in that document - [ ] **A way to send yourself a notification** · free tiers are ample at this volume - Why: Alerts that only appear in a page you have to open are not alerts. - Get it: Any transactional sender, or a local mail relay you already run. - Verify: A threshold crossing reaches your inbox with the retrieval time in it ## 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: - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. - maintained stock coverage and editorial calls - intraday data quality and provider reliability - managed earnings and price alert delivery - the product's target-price methodology - AI-agent API access If one of those is essential to you, that is the reason to keep paying for StocksBrew, and the README should say so rather than pretend.
# Build brief · StocksBrew 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 personal watchlist that shows the rule hits and the filings behind a stance and never tells you to buy anything to replace StocksBrew. 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 Two constraints decide this build, and both are outside the code. First, do not build something that gives investment advice, and do not build something that looks like it does. Under the United States Investment Advisers Act the definition of an investment adviser at 15 U.S.C. 80b-2(a)(11) excludes, at clause (D), the publisher of a bona fide newspaper, news magazine or business or financial publication of general and regular circulation, and in Lowe v. SEC the Supreme Court read that exclusion as covering impersonal advice that is not tailored to any specific client's portfolio. A private tool you run for yourself is not the problem. Tailored output, shown to other people for money, is a different thing entirely and is where that line sits. So the output of this build is rule hits and cited documents, never a verdict, never a price target, and never a number presented as a forecast. If you cannot point at the filing, do not print the sentence. Second, market data is licensed rather than free. Exchange data policies draw the boundary at a delay: Nasdaq's published policies treat 15 minutes as the default period after which information becomes delayed information, require a signed agreement and monthly reporting from anyone redistributing even that delayed data, and require prior approval before charging for it. Your data provider's terms will say something similar. Everything here therefore stays on your machine, every figure on screen carries its source and the timestamp it was retrieved at, and there is no sharing feature. ### Stack (fixed, do not substitute) - Runtime: Node with a server-rendered interface on the loopback address, plus a scheduled refresh · one reader, no accounts, and nothing that could be mistaken for a publication - Storage: SQLite, one file, with every fetched document retained · the citation is the product and a citation you cannot reopen is not one - Prices: One provider whose free quota you have read, with an explicit request budget · a daily quota measured in tens of requests changes the whole refresh design - Filings: The regulator's own full text search and submission feeds, with a declared user agent · the filings are public, free, and the only primary source in the build - Rules: One plain file of named rules, each producing a hit with its inputs · a score nobody can decompose is a number you will end up trusting ### Data model (create this before Phase 1) - tickers(symbol, name, cik, added_at, active) · the cik is what joins a ticker to its filings - price_snapshots(symbol, as_of_date, close, volume, source, retrieved_at, delayed) · delayed records what the provider said it was giving you - filings(id, cik, form_type, filed_at, accession, url, title, retrieved_at, body_sha256) - news_items(id, symbol, source_url, title, published_at, retrieved_at, dedup_key) - rule_hits(id, symbol, rule_name, as_of_date, inputs_json, fired, explanation) · every rule writes a row whether it fired or not - stances(symbol, as_of_date, stance, hit_ids_json, computed_at) · the stance is a function of hit_ids and nothing else - alerts(id, symbol, kind, threshold_json, last_fired_at, cooldown_minutes, muted) - requests(id, at, provider, endpoint, symbol, status, quota_remaining) · the request budget only works if every call is counted - There is no column called target_price and no column called recommendation. The stance is a label over rule hits, and removing the hits removes the label. ### Phase 1 · A refresh that fits inside the quota Build: Read your provider's current published free allowance yourself before designing the refresh: when checked in September 2026 one widely used provider's free key allowed 25 requests per day, which is tens of symbols, not hundreds. Count every request in a table, cap the watchlist accordingly, and refuse to add a ticker the budget cannot serve rather than degrading everyone's data silently. Trap: Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. Done when: A full refresh of the configured watchlist consumes fewer requests than the configured daily allowance, asserted by counting rows in the requests table. Adding a ticker beyond the budget is refused with the arithmetic shown, tested at the boundary. A symbol that failed to refresh is marked as such and its rules do not run that day, tested by stubbing a failure. Every stored price carries its source, its retrieval time and whether the provider described it as delayed. Do not build yet: No filings, no news, no rules, no alerts. ### Phase 2 · Filings and news that can be reopened Build: Fetch filings from the regulator's own feeds keyed by the company identifier, declaring a real contact in the user agent and staying inside the published request rate, which was 10 requests per second when checked in September 2026. Store the URL, the filing date, the retrieval time and a hash of the body. Deduplicate news by a stable key so the same story from two feeds appears once. Trap: Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. Done when: Every filing row has a URL, a filing date, a retrieval time and a body hash, asserted by a schema test that rejects nulls. The client declares a contact in its user agent and stays inside the published rate, tested with a fake clock. The same news item arriving from two feeds produces one row, tested with a duplicate fixture. A document whose retrieval failed is recorded as a failed attempt rather than omitted silently. Do not build yet: No rules, no stances, no alerts. ### Phase 3 · Rules that show their work Build: One file of named rules, each a pure function over stored data returning fired or not, its inputs and a one-sentence explanation. Write a row for every rule evaluated, including the ones that did not fire. Compose the stance from the hits and store the hit identifiers with it. Refuse to show a stance when any input for that symbol is stale beyond a configured age. Trap: Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. Done when: Every rule evaluation writes a row including non-firing rules, checked in the database after a refresh. A stance displays with the full list of hits that produced it and cannot render without them, asserted in the suite. Changing one rule's threshold changes exactly the stances whose hits involved that rule, tested with a fixture. A symbol with stale inputs shows the staleness instead of a stance, tested at the boundary. Do not build yet: No alerts, no export, no operational documentation. ### Phase 4 · Alerts that fire once and say how old the data is Build: Per-ticker price thresholds and an upcoming filing or earnings reminder. Fire on a crossing rather than on a level, so a price sitting at the threshold does not notify every refresh. Include the retrieval time and whether the data was delayed in the message body. A cooldown per alert, and a mute that is a stored state rather than a deleted row. Trap: Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. Done when: A price sitting past its threshold across several refreshes produces one notification, tested with a repeated fixture. Every notification body contains the retrieval time and the delayed flag of the data it was computed from, asserted in the suite. Muting an alert preserves its configuration and history, and unmuting does not fire retrospectively. A notification send failure is retried and then surfaced in the interface rather than dropped. Do not build yet: No export, no retention, no disclaimer work yet. ### Phase 5 · Staleness, export and the line you do not cross Build: A staleness badge on every figure computed from its retrieval time. Export tickers, prices, filings, hits, stances and alerts as CSV, plus the stored documents. No sharing feature, no public page, no syndication route, and a disclaimer on every page rather than only in the README. Track the provider request count against the allowance on the same page. Trap: Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. Done when: Every displayed figure carries a staleness indicator derived from its retrieval time, checked across the watchlist and detail views. The application binds to the loopback address only and refuses to start otherwise with an explanation, tested. Export produces every table plus the stored documents and reimports into an empty database with equal counts. The disclaimer is rendered by the layout, so no page can be added without it, asserted in the suite. Do not build yet: Nothing after this. ### Out of scope (and why) - Price targets, forecasts and anything resembling a recommendation, for the reasons in rule zero and because the build has no basis for any of them. - Intraday and real-time quotes, which are licensed products with agreements attached rather than a different endpoint on the same key. - Brokerage connections and order placement, which turn a reading tool into something that can lose money by accident. - Sharing, publishing or syndicating the output, which is both a redistribution question about the data and the line between a private tool and a publication. ### README must contain - That the output is rule hits and cited documents, never advice, with the disclaimer reproduced in full - That market data is licensed, that the tool is local only, and that there is deliberately no sharing route - The provider's free allowance you designed around, the date you checked it, and how many tickers it supports - That filings are fetched with a declared contact inside the published request rate, with that rate and its date - How staleness is computed and why a stale input suppresses a stance rather than dimming it
# Agent instructions · StocksBrew indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node with a server-rendered interface on the loopback address, plus a scheduled refresh, SQLite, one file, with every fetched document retained, One provider whose free quota you have read, with an explicit request budget, The regulator's own full text search and submission feeds, with a declared user agent, One plain file of named rules, each producing a hit with its inputs. 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 - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time.
# Build plan · StocksBrew A personal research cockpit whose hard problems are producing a stance that is nothing more than a visible sum of stated rules, keeping every displayed number tied to a source and a retrieval time, and fitting a daily refresh inside a data quota that is far smaller than the watchlist wants to be. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A refresh that fits inside the quota A daily update of the whole watchlist stays inside the provider's published free allowance, with every call counted. ### Steps 1. Read the provider's published allowance and write it down The refresh design follows from this number, not the other way round. Files: `config/providers.json`, `docs/quota.md` 2. Count every request in a table before making it A budget you cannot audit is not a budget. Files: `src/data/budget.js`, `tests/budget.test.js` 3. Refuse a ticker the budget cannot serve Silent degradation is worse than a clear refusal. Files: `src/watchlist/add.js` ### Done when - [ ] A full refresh stays inside the daily allowance - [ ] An unaffordable ticker is refused with the arithmetic - [ ] Failed symbols are marked and skipped by rules - [ ] Every price carries source, time and delay status ### Watch out - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. ## Phase 2 · Filings and news that can be reopened Every document the tool mentions is stored with its URL, its retrieval time and a hash of what was retrieved. ### Steps 1. Declare who you are and respect the published rate The regulator publishes both and expects both. Files: `src/data/edgar.js` 2. Store the URL, the time and the hash A citation you cannot reopen is a claim. Files: `src/data/filings.js`, `tests/filings.test.js` 3. Deduplicate news on a stable key The same story from three feeds is one event. Files: `src/data/news.js` ### Done when - [ ] Filings carry URL, date, retrieval time and hash - [ ] The client declares a contact and honours the rate - [ ] Duplicate news items collapse to one row - [ ] Failed retrievals are recorded, not omitted ### Watch out - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. ## Phase 3 · Rules that show their work A stance is a visible function of named rule hits, and no hits means no stance. ### Steps 1. Keep every rule pure and named in one file You will read this file more than any other. Files: `src/rules/rules.js`, `tests/rules.test.js` 2. Record non-firing rules too What did not fire is half the explanation. Files: `src/rules/evaluate.js` 3. Make a stale input suppress the stance A stance computed from Tuesday's price is fiction. Files: `src/rules/staleness.js` ### Done when - [ ] Every evaluated rule writes a row - [ ] A stance cannot render without its hits - [ ] A threshold change moves only the related stances - [ ] Stale inputs suppress the stance ### Watch out - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. ## Phase 4 · Alerts that fire once and say how old the data is A threshold crossing notifies you exactly once, with the retrieval time in the message. ### Steps 1. Fire on a crossing, not on a level A standing condition is not news after the first time. Files: `src/alerts/crossing.js`, `tests/crossing.test.js` 2. Put the retrieval time in the message Acting on a figure means knowing how old it is. Files: `src/alerts/message.js` 3. Mute as a state, never as a deletion You will want the history back later. Files: `src/alerts/mute.js` ### Done when - [ ] A standing condition notifies once - [ ] Messages carry retrieval time and delay status - [ ] Muting preserves configuration and history - [ ] Send failures are retried and surfaced ### Watch out - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ## Not in this build - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. ## After v1, if you want it - A rule backtest over your own stored history, reported with its sample size, which is the only honest way to judge a rule - A quarterly review that lists the rules that never fired, which is usually most of them - A second price source used only to cross-check the first, which catches a bad print faster than any rule does
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Prices, filings, news, hits and stances. One file to copy. DATABASE_PATH=./data/research.db # Required · secret. Market data provider. Its allowance is the constraint the refresh is built around. PRICE_API_KEY=redacted # Required. The provider's published free allowance. Recheck it and update this. DAILY_REQUEST_ALLOWANCE=25 # Required. Declared on every filing request, as the service asks. EDGAR_USER_AGENT=Your Name your.address@example.com # Required. Beyond this an input is stale and its symbol shows no stance. STALE_AFTER_HOURS=30 # Required · secret. Where alerts go. Nothing else leaves the machine. SMTP_URL=smtps://user:redacted@mail.example.com:465 # Required. Loopback only. The server refuses to start on anything else. BIND_ADDRESS=127.0.0.1
# StocksBrew · product brief ## Problem A personal stock-research cockpit is very buildable, but StocksBrew's useful current calls depend on continuously maintained market, filings, news, and alert pipelines. A DIY version can make its evidence and rules explicit; it will not inherit the service's coverage, refresh reliability, or editorial judgment. ## Product outcome A private cockpit whose every number carries a source and an age, whose stance is a visible sum of named rules, and which has no route by which it could become a publication or a redistribution of licensed data. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - market-data API key - SEC EDGAR data - RSS news sources - LLM API key - scheduled job ## Explicit non-goals for v1 - Price targets, forecasts and recommendations. - Intraday and real-time quotes. - Brokerage connections and trading. - Any sharing, publishing or syndication route. - maintained stock coverage and editorial calls - intraday data quality and provider reliability - managed earnings and price alert delivery - the product's target-price methodology - AI-agent API access ## Success criteria - A full refresh is confirmed to stay inside the daily allowance - Every filing is confirmed to carry URL, retrieval time and hash - A stance is confirmed unable to render without its hits - A standing threshold is confirmed to notify once - The server is confirmed to refuse a non-loopback bind
# Build brief · StocksBrew 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 personal watchlist that shows the rule hits and the filings behind a stance and never tells you to buy anything to replace StocksBrew. 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 Two constraints decide this build, and both are outside the code. First, do not build something that gives investment advice, and do not build something that looks like it does. Under the United States Investment Advisers Act the definition of an investment adviser at 15 U.S.C. 80b-2(a)(11) excludes, at clause (D), the publisher of a bona fide newspaper, news magazine or business or financial publication of general and regular circulation, and in Lowe v. SEC the Supreme Court read that exclusion as covering impersonal advice that is not tailored to any specific client's portfolio. A private tool you run for yourself is not the problem. Tailored output, shown to other people for money, is a different thing entirely and is where that line sits. So the output of this build is rule hits and cited documents, never a verdict, never a price target, and never a number presented as a forecast. If you cannot point at the filing, do not print the sentence. Second, market data is licensed rather than free. Exchange data policies draw the boundary at a delay: Nasdaq's published policies treat 15 minutes as the default period after which information becomes delayed information, require a signed agreement and monthly reporting from anyone redistributing even that delayed data, and require prior approval before charging for it. Your data provider's terms will say something similar. Everything here therefore stays on your machine, every figure on screen carries its source and the timestamp it was retrieved at, and there is no sharing feature. ### Stack (fixed, do not substitute) - Runtime: Node with a server-rendered interface on the loopback address, plus a scheduled refresh · one reader, no accounts, and nothing that could be mistaken for a publication - Storage: SQLite, one file, with every fetched document retained · the citation is the product and a citation you cannot reopen is not one - Prices: One provider whose free quota you have read, with an explicit request budget · a daily quota measured in tens of requests changes the whole refresh design - Filings: The regulator's own full text search and submission feeds, with a declared user agent · the filings are public, free, and the only primary source in the build - Rules: One plain file of named rules, each producing a hit with its inputs · a score nobody can decompose is a number you will end up trusting ### Data model (create this before Phase 1) - tickers(symbol, name, cik, added_at, active) · the cik is what joins a ticker to its filings - price_snapshots(symbol, as_of_date, close, volume, source, retrieved_at, delayed) · delayed records what the provider said it was giving you - filings(id, cik, form_type, filed_at, accession, url, title, retrieved_at, body_sha256) - news_items(id, symbol, source_url, title, published_at, retrieved_at, dedup_key) - rule_hits(id, symbol, rule_name, as_of_date, inputs_json, fired, explanation) · every rule writes a row whether it fired or not - stances(symbol, as_of_date, stance, hit_ids_json, computed_at) · the stance is a function of hit_ids and nothing else - alerts(id, symbol, kind, threshold_json, last_fired_at, cooldown_minutes, muted) - requests(id, at, provider, endpoint, symbol, status, quota_remaining) · the request budget only works if every call is counted - There is no column called target_price and no column called recommendation. The stance is a label over rule hits, and removing the hits removes the label. ### Phase 1 · A refresh that fits inside the quota Build: Read your provider's current published free allowance yourself before designing the refresh: when checked in September 2026 one widely used provider's free key allowed 25 requests per day, which is tens of symbols, not hundreds. Count every request in a table, cap the watchlist accordingly, and refuse to add a ticker the budget cannot serve rather than degrading everyone's data silently. Trap: Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. Done when: A full refresh of the configured watchlist consumes fewer requests than the configured daily allowance, asserted by counting rows in the requests table. Adding a ticker beyond the budget is refused with the arithmetic shown, tested at the boundary. A symbol that failed to refresh is marked as such and its rules do not run that day, tested by stubbing a failure. Every stored price carries its source, its retrieval time and whether the provider described it as delayed. Do not build yet: No filings, no news, no rules, no alerts. ### Phase 2 · Filings and news that can be reopened Build: Fetch filings from the regulator's own feeds keyed by the company identifier, declaring a real contact in the user agent and staying inside the published request rate, which was 10 requests per second when checked in September 2026. Store the URL, the filing date, the retrieval time and a hash of the body. Deduplicate news by a stable key so the same story from two feeds appears once. Trap: Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. Done when: Every filing row has a URL, a filing date, a retrieval time and a body hash, asserted by a schema test that rejects nulls. The client declares a contact in its user agent and stays inside the published rate, tested with a fake clock. The same news item arriving from two feeds produces one row, tested with a duplicate fixture. A document whose retrieval failed is recorded as a failed attempt rather than omitted silently. Do not build yet: No rules, no stances, no alerts. ### Phase 3 · Rules that show their work Build: One file of named rules, each a pure function over stored data returning fired or not, its inputs and a one-sentence explanation. Write a row for every rule evaluated, including the ones that did not fire. Compose the stance from the hits and store the hit identifiers with it. Refuse to show a stance when any input for that symbol is stale beyond a configured age. Trap: Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. Done when: Every rule evaluation writes a row including non-firing rules, checked in the database after a refresh. A stance displays with the full list of hits that produced it and cannot render without them, asserted in the suite. Changing one rule's threshold changes exactly the stances whose hits involved that rule, tested with a fixture. A symbol with stale inputs shows the staleness instead of a stance, tested at the boundary. Do not build yet: No alerts, no export, no operational documentation. ### Phase 4 · Alerts that fire once and say how old the data is Build: Per-ticker price thresholds and an upcoming filing or earnings reminder. Fire on a crossing rather than on a level, so a price sitting at the threshold does not notify every refresh. Include the retrieval time and whether the data was delayed in the message body. A cooldown per alert, and a mute that is a stored state rather than a deleted row. Trap: Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. Done when: A price sitting past its threshold across several refreshes produces one notification, tested with a repeated fixture. Every notification body contains the retrieval time and the delayed flag of the data it was computed from, asserted in the suite. Muting an alert preserves its configuration and history, and unmuting does not fire retrospectively. A notification send failure is retried and then surfaced in the interface rather than dropped. Do not build yet: No export, no retention, no disclaimer work yet. ### Phase 5 · Staleness, export and the line you do not cross Build: A staleness badge on every figure computed from its retrieval time. Export tickers, prices, filings, hits, stances and alerts as CSV, plus the stored documents. No sharing feature, no public page, no syndication route, and a disclaimer on every page rather than only in the README. Track the provider request count against the allowance on the same page. Trap: Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about. Done when: Every displayed figure carries a staleness indicator derived from its retrieval time, checked across the watchlist and detail views. The application binds to the loopback address only and refuses to start otherwise with an explanation, tested. Export produces every table plus the stored documents and reimports into an empty database with equal counts. The disclaimer is rendered by the layout, so no page can be added without it, asserted in the suite. Do not build yet: Nothing after this. ### Out of scope (and why) - Price targets, forecasts and anything resembling a recommendation, for the reasons in rule zero and because the build has no basis for any of them. - Intraday and real-time quotes, which are licensed products with agreements attached rather than a different endpoint on the same key. - Brokerage connections and order placement, which turn a reading tool into something that can lose money by accident. - Sharing, publishing or syndicating the output, which is both a redistribution question about the data and the line between a private tool and a publication. ### README must contain - That the output is rule hits and cited documents, never advice, with the disclaimer reproduced in full - That market data is licensed, that the tool is local only, and that there is deliberately no sharing route - The provider's free allowance you designed around, the date you checked it, and how many tickers it supports - That filings are fetched with a declared contact inside the published request rate, with that rate and its date - How staleness is computed and why a stale input suppresses a stance rather than dimming it
# Architecture · StocksBrew ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node with a server-rendered interface on the loopback address, plus a scheduled refresh | one reader, no accounts, and nothing that could be mistaken for a publication | | Storage | SQLite, one file, with every fetched document retained | the citation is the product and a citation you cannot reopen is not one | | Prices | One provider whose free quota you have read, with an explicit request budget | a daily quota measured in tens of requests changes the whole refresh design | | Filings | The regulator's own full text search and submission feeds, with a declared user agent | the filings are public, free, and the only primary source in the build | | Rules | One plain file of named rules, each producing a hit with its inputs | a score nobody can decompose is a number you will end up trusting | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Budget | Request counting, allowance enforcement and refusals. | Any provider, provided the allowance is read and the counting stays. | | Sources | Filings, news, retrieval times and body hashes. | Any feed, provided documents remain reopenable. | | Rules | Named pure rules and hit rows for every evaluation. | Any rule set, provided non-firing rules are recorded too. | | Stance | Composition from hits and staleness suppression. | Not swappable for a weighted score without the hits beside it. | | Alerts | Crossings, cooldowns and messages carrying data age. | Any channel, provided the message states how old the data was. | ## 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 · Prices, filings, news, hits and stances. One file to copy. - `PRICE_API_KEY` · required, secret · Market data provider. Its allowance is the constraint the refresh is built around. - `DAILY_REQUEST_ALLOWANCE` · required · The provider's published free allowance. Recheck it and update this. - `EDGAR_USER_AGENT` · required · Declared on every filing request, as the service asks. - `STALE_AFTER_HOURS` · required · Beyond this an input is stale and its symbol shows no stance. - `SMTP_URL` · required, secret · Where alerts go. Nothing else leaves the machine. - `BIND_ADDRESS` · required · Loopback only. The server refuses to start on anything else. ## 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 · StocksBrew product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node with a server-rendered interface on the loopback address, plus a scheduled refresh, SQLite, one file, with every fetched document retained, One provider whose free quota you have read, with an explicit request budget, The regulator's own full text search and submission feeds, with a declared user agent, One plain file of named rules, each producing a hit with its inputs. - 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 - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. - Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about.
# Delivery milestones · StocksBrew Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A refresh that fits inside the quota A daily update of the whole watchlist stays inside the provider's published free allowance, with every call counted. ### Steps 1. Read the provider's published allowance and write it down The refresh design follows from this number, not the other way round. Files: `config/providers.json`, `docs/quota.md` 2. Count every request in a table before making it A budget you cannot audit is not a budget. Files: `src/data/budget.js`, `tests/budget.test.js` 3. Refuse a ticker the budget cannot serve Silent degradation is worse than a clear refusal. Files: `src/watchlist/add.js` ### Done when - [ ] A full refresh stays inside the daily allowance - [ ] An unaffordable ticker is refused with the arithmetic - [ ] Failed symbols are marked and skipped by rules - [ ] Every price carries source, time and delay status ### Watch out - Adding a retry loop to a provider with a daily allowance burns the day's budget on one failing symbol and leaves the rest of the watchlist with yesterday's prices, which then feed rules that report as though they ran. Count requests before making them, retry at most once, and mark a symbol as not refreshed rather than letting stale data flow into a rule. ## M2 · Filings and news that can be reopened Every document the tool mentions is stored with its URL, its retrieval time and a hash of what was retrieved. ### Steps 1. Declare who you are and respect the published rate The regulator publishes both and expects both. Files: `src/data/edgar.js` 2. Store the URL, the time and the hash A citation you cannot reopen is a claim. Files: `src/data/filings.js`, `tests/filings.test.js` 3. Deduplicate news on a stable key The same story from three feeds is one event. Files: `src/data/news.js` ### Done when - [ ] Filings carry URL, date, retrieval time and hash - [ ] The client declares a contact and honours the rate - [ ] Duplicate news items collapse to one row - [ ] Failed retrievals are recorded, not omitted ### Watch out - Fetching a filing, extracting a sentence and keeping only the sentence produces a citation that cannot be checked three months later when the page has moved, and by then you no longer remember whether the tool or the source was wrong. Store the URL, the retrieval time and a hash of the body you actually read, and make the interface link to your stored copy alongside the original. ## M3 · Rules that show their work A stance is a visible function of named rule hits, and no hits means no stance. ### Steps 1. Keep every rule pure and named in one file You will read this file more than any other. Files: `src/rules/rules.js`, `tests/rules.test.js` 2. Record non-firing rules too What did not fire is half the explanation. Files: `src/rules/evaluate.js` 3. Make a stale input suppress the stance A stance computed from Tuesday's price is fiction. Files: `src/rules/staleness.js` ### Done when - [ ] Every evaluated rule writes a row - [ ] A stance cannot render without its hits - [ ] A threshold change moves only the related stances - [ ] Stale inputs suppress the stance ### Watch out - Blending rule outputs into a weighted score produces a number that looks like a measurement and cannot be argued with, and within a fortnight you are acting on a figure whose weights you chose arbitrarily on a Tuesday. Keep the stance as a named combination of named hits, show every hit including the ones that did not fire, and never display a composite number without the hits beside it. ## M4 · Alerts that fire once and say how old the data is A threshold crossing notifies you exactly once, with the retrieval time in the message. ### Steps 1. Fire on a crossing, not on a level A standing condition is not news after the first time. Files: `src/alerts/crossing.js`, `tests/crossing.test.js` 2. Put the retrieval time in the message Acting on a figure means knowing how old it is. Files: `src/alerts/message.js` 3. Mute as a state, never as a deletion You will want the history back later. Files: `src/alerts/mute.js` ### Done when - [ ] A standing condition notifies once - [ ] Messages carry retrieval time and delay status - [ ] Muting preserves configuration and history - [ ] Send failures are retried and surfaced ### Watch out - Alerting whenever the current value is past the threshold sends an identical message on every refresh for as long as the condition holds, which trains you to ignore the channel within two days. Compare the previous stored value with the current one and fire only on the transition, with a cooldown and a stored last-fired time. ## M5 · Staleness, export and the line you do not cross (production only) Old data is visibly old, everything exports, and the build refuses to become a publication. ### Steps 1. Derive staleness from retrieval time everywhere The age of a number is part of the number. Files: `src/web/staleness.js` 2. Bind to loopback and refuse anything else This is the technical form of rule zero. Files: `src/server.js`, `tests/loopback-only.test.js` 3. Put the disclaimer in the layout, not the README A page added next month should inherit it. Files: `src/web/layout.js` ### Done when - [ ] Every figure shows its staleness - [ ] The server refuses a non-loopback bind - [ ] Export and import round-trip tables and documents - [ ] The disclaimer is inherited by every page ### Watch out - Adding a read-only public page because it seems harmless turns a private tool into redistribution of somebody else's licensed data and into output shown to other people, which are the two lines rule zero exists to keep you behind. Leave the service bound to the loopback address, put the disclaimer in the page layout so it cannot be forgotten, and make the export the sharing mechanism for a human to think about.
# Operations · StocksBrew ## Backup The SQLite file and the stored documents together. The prices can be refetched within the allowance; the filings you already retrieved and hashed are the part that makes an old stance explainable. ## Restore Restore, run one refresh, and confirm that request counts match the expectation and that stances recompute to the same labels from the same hit identifiers. A different stance from the same hits means a rule changed and was not recorded. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Requests used against the allowance, the oldest retrieval time in the watchlist, the count of symbols suppressed for staleness, and notification failures. The staleness count is the one that reveals a quota you have quietly outgrown. ## Incident checklist The incident here is not an outage, it is a decision taken on a figure that was older or more delayed than it appeared. Retrieval times on every row and the delayed flag from the provider are what let you reconstruct what the screen actually said that morning. 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 - [ ] A full refresh is confirmed to stay inside the daily allowance - [ ] Every filing is confirmed to carry URL, retrieval time and hash - [ ] A stance is confirmed unable to render without its hits - [ ] A standing threshold is confirmed to notify once - [ ] The server is confirmed to refuse a non-loopback bind ## Launch constraint Do not market omitted StocksBrew 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. Prices, filings, news, hits and stances. One file to copy. DATABASE_PATH=./data/research.db # Required · secret. Market data provider. Its allowance is the constraint the refresh is built around. PRICE_API_KEY=redacted # Required. The provider's published free allowance. Recheck it and update this. DAILY_REQUEST_ALLOWANCE=25 # Required. Declared on every filing request, as the service asks. EDGAR_USER_AGENT=Your Name your.address@example.com # Required. Beyond this an input is stale and its symbol shows no stance. STALE_AFTER_HOURS=30 # Required · secret. Where alerts go. Nothing else leaves the machine. SMTP_URL=smtps://user:redacted@mail.example.com:465 # Required. Loopback only. The server refuses to start on anything else. BIND_ADDRESS=127.0.0.1
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay for a maintained, current opinionated workflow: a stock call, the evidence behind it, a watchlist, and alerts that arrive without running or debugging a personal data pipeline.
xmaintained stock coverage and editorial calls
xintraday data quality and provider reliability
xmanaged earnings and price alert delivery
xthe product's target-price methodology
xAI-agent API access
StocksBrew pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | Up to 3 Watchlist stocks; research preview; stock search; no card |
| pro | $9 | $8.25 | Unlimited Watchlist; full calls/zones/drivers/risks; targets; alerts; MCP and REST agent access |
free tierUp to 3 Watchlist stocks; stock search and research preview; no card
billingmonthly + annual ($99/year, save 8%); 3-day trial
verified 2026-08-13 · source ↗
Vibecode StocksBrew
Kinda. The core of StocksBrew is buildable in a weekend with the prompt on this page, but there are real gaps: maintained stock coverage and editorial calls, intraday data quality and provider reliability. Read the honest list above before committing.
How much does StocksBrew cost?
StocksBrew costs about $9/month (Pro, checked 2026-08-02), which is $108 per year.
What do I lose by replacing StocksBrew?
Honestly: maintained stock coverage and editorial calls; intraday data quality and provider reliability; managed earnings and price alert delivery; the product's target-price methodology; AI-agent API access. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to StocksBrew?
Yes: OpenBB Platform (Open-source financial research platform with market-data integrations.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.