Build StocksBrew
KINDAreplaces $9/mosaves $108/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the local interface and the scheduled refresh.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v22 or higher - API keyfree 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 - decidefree
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 - decidefree
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 - API keyfree 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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/research.db | Prices, filings, news, hits and stances. One file to copy. |
PRICE_API_KEYsecret | required | redacted | Market data provider. Its allowance is the constraint the refresh is built around. |
DAILY_REQUEST_ALLOWANCE | required | 25 | The provider's published free allowance. Recheck it and update this. |
EDGAR_USER_AGENT | required | Your Name your.address@example.com | Declared on every filing request, as the service asks. |
STALE_AFTER_HOURS | required | 30 | Beyond this an input is stale and its symbol shows no stance. |
SMTP_URLsecret | required | smtps://user:redacted@mail.example.com:465 | Where alerts go. Nothing else leaves the machine. |
BIND_ADDRESS | required | 127.0.0.1 | Loopback only. The server refuses to start on anything else. |
The build, in order
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.
The refresh design follows from this number, not the other way round.
Files
config/providers.jsondocs/quota.mdA budget you cannot audit is not a budget.
Files
src/data/budget.jstests/budget.test.jsSilent degradation is worse than a clear refusal.
Files
src/watchlist/add.js
done when · tick each as it passeswatch 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.
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.
The regulator publishes both and expects both.
Files
src/data/edgar.jsA citation you cannot reopen is a claim.
Files
src/data/filings.jstests/filings.test.jsThe same story from three feeds is one event.
Files
src/data/news.js
done when · tick each as it passeswatch 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.
Rules that show their work
A stance is a visible function of named rule hits, and no hits means no stance.
You will read this file more than any other.
Files
src/rules/rules.jstests/rules.test.jsWhat did not fire is half the explanation.
Files
src/rules/evaluate.jsA stance computed from Tuesday's price is fiction.
Files
src/rules/staleness.js
done when · tick each as it passeswatch 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.
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.
A standing condition is not news after the first time.
Files
src/alerts/crossing.jstests/crossing.test.jsActing on a figure means knowing how old it is.
Files
src/alerts/message.jsYou will want the history back later.
Files
src/alerts/mute.js
done when · tick each as it passeswatch 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.
Staleness, export and the line you do not crossproduct builder
Old data is visibly old, everything exports, and the build refuses to become a publication.
The age of a number is part of the number.
Files
src/web/staleness.jsThis is the technical form of rule zero.
Files
src/server.jstests/loopback-only.test.jsA page added next month should inherit it.
Files
src/web/layout.js
done when · tick each as it passeswatch 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.
That is the whole plan for StocksBrew. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more personal finance.