A single-user investment research and monitoring system: it ingests SEC filings, market data, institutional and insider disclosures, and news; scores companies against a written investment mandate; and queues trades that a human approves before anything is sent to a broker.
Built as a personal tool, so it optimises for one thing that most portfolio dashboards do not: refusing to state more than the data supports. A metric that does not apply to a business type is reported as not evaluable rather than scored badly. A feed that is four months stale renders its own age next to the number. A capped list says what it left out.
204 modules in lib/ |
Shared by both hosts. No Next.js imports, so the worker can use all of it. |
| 72 API routes | All session-gated in middleware.ts except the login flow and cron. |
| 67 React components | The terminal UI, in components/terminal/. |
| 24 scheduled worker tasks | Wall-clock anchored so an hourly cohort wakes in one burst. |
| 878 tests | npm test. No database, no network. |
Two hosts, one repository, and a database that is the only thing they share.
Next.js on Vercel Long-lived worker (worker/)
app/api/* 24 scheduled tasks
components/terminal/* imports the SAME lib/
\ /
lib/* (no Next.js imports, so both hosts can use it)
|
Postgres
|
Alpaca Finnhub FRED SEC EDGAR Treasury SnapTrade Wikimedia Greenhouse Lever
The split matters for one reason: the trade-capable broker credential exists only on the worker. The web app physically cannot place an order, because the client that could is never imported into anything Vercel builds. A test asserts that boundary rather than trusting it.
lib/orders/ and the order_intents table. The intent row is the security boundary of the
trading feature, so the guarantees are properties of the row rather than of the code that writes it:
max_spendis a GENERATED column, so an unbounded buy is not representable. A cap on a share count is not a cap on money, which is the mistake this replaced.- A
CHECKconstraint caps any single order at $25,000. No environment variable can raise it. - Approval posts the fingerprint of the row the client actually rendered, and the server matches it
inside the
UPDATE'sWHEREclause. The failure this prevents is not misreading a number, it is an approval landing on a row nobody read. - Every state change writes its audit row in the same statement, as a data-modifying CTE. Two statements can come apart, and what survives is a row claiming to be approved with nothing saying by whom or of what.
- Every transition guards on the status it moves from, in SQL, never in JavaScript first. Read-decide-write is a race that two clicks both win.
lib/orders/submit.ts, on ambiguity. A lost HTTP response is not a failed order. An unknown
outcome stays in submitting and reconciles against the broker on the order id, because marking it
failed would refund the budget for money that may already have left.
lib/analyst/businessType.ts. The scoring engine was judging a bank's figures like a software
company's. A bank's operating cash flow was -$147.78B in a profitable year, which is ordinary for
a bank and catastrophic for anything else. Every company is now read as the kind of business it is,
and where the mandate does not describe that business the engine says so instead of scoring it low.
lib/scorecard/. A deterministic five-criterion tag on every alert, arithmetic rather than a
model. It is context on a finding and never a gate: filtering belongs on discovery, not on facts.
lib/userAgent.ts. A small one, but it is the shape of a class of bug. SEC rejects a
tool-sounding User-Agent with a 403 whose HTML body parses downstream as "no filings found," which
is indistinguishable from a company that never filed. So the header is required and throws when
unset, rather than defaulting. A missing config should not be able to look like a fact.
npm install
cp .env.example .env.local # fill in DATABASE_URL, the API keys, SEC_USER_AGENT
npm run db:migrate
npm run dev # http://localhost:3000
npm run worker # the scheduled tasks, separately
npm test # no database, no networkEvery provider except the LLM has a usable free tier, and the alternative-data collectors (Wikipedia pageviews, Greenhouse and Lever job boards) need no key at all.
SEC_USER_AGENT must be a real name and email. See lib/userAgent.ts for why it has no fallback.
This is one person's tool, not a product. It is not multi-tenant, it has one password, and it gives no investment advice: it reports, measures and compares, and every decision and every order is a human's. The read-only brokerage link is read-only permanently.
Source comments occasionally reference CLAUDE.md and HANDOFF.md. Those are the engineering
journal and they stay in a private repository, along with the phase plans, because they carry
deployment detail specific to my running instance.