Skip to content

Repository files navigation

AXIS Terminal

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.

What is in here

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.

Architecture

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.

The parts worth reading

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_spend is 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 CHECK constraint 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's WHERE clause. 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.

Setup

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 network

Every 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.

Scope and honesty

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.

About

Single-user investment research and monitoring system: SEC filings, market data and disclosures scored against a written mandate, with human-approved trade execution. Next.js + Postgres + a scheduled worker.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages