Use GitHub's private reporting: Security → Report a vulnerability on this repository. It reaches the maintainers only. Please do not open a public issue for anything that would let a caller read data, escalate a tier, or reach another account's records.
There is also an in-product path: the submit_feedback MCP tool attaches the failing
call's context automatically, which is usually faster than describing it.
Publishing the server is deliberate — the methodology is the product, and a backtest you cannot audit is a claim, not a measurement. But some things are absent on purpose, and their absence is not an oversight:
- No market data. No prices, no chains, no fixtures containing either.
- No ingestion pipeline. How the data is sourced, normalised and gap-repaired is not
here, and the loaders under
data/that read the private upstream tables are not here. - No credentials. Every secret is read from the environment. The only
sk_live_strings anywhere are test fixtures likesk_live_not_a_real_key. - No production topology.
docker-compose.example.ymlis a self-hosting template, not a description of the deployed system.
Reading the code should make these verifiable rather than requiring trust:
API keys are scrypt-hashed with a pepper from STRATIFY_KEY_PEPPER, compared with
hmac.compare_digest. Only the hash is stored, and server/run.sh refuses to start if the
pepper is unset — the in-code default is a development value published in this repository
and is unsafe by construction.
Tier isolation is enforced in ClickHouse, not in Python. Each plan binds a distinct
database user (stratify_free, stratify_paid) whose visible date range is a row policy
(data/schema/70_tiers.sql). A bug in the application layer cannot widen it. The
signal cache is keyed on the ClickHouse user for the same reason — see the comment in
engine/signals.py, which exists because keying on entry minute alone once let a free-tier
gate evaluate against seven years of paid history.
No eval, no string-built SQL from user input. The strategy protocol is a closed
vocabulary; unrecognised fields are refused rather than ignored. Queries are parameterised.
No route returns market data. A test asserts no such route exists and another asserts a result carries no per-contract prices. Per-trade prices are released only through a three-control boundary: a minimum trade count, a per-response cap, and an hourly meter.
The admin console cannot bootstrap itself. The first admin is granted by
python -m server.grant_admin, which requires shell access to the host, because any
in-app path to creating the first admin is a privilege-escalation route by definition.
is_admin is re-read from the database on every admin request, so revoking it takes effect
on the next click rather than at the next sign-in.
X-Forwarded-For is trusted only from the proxy. STRATIFY_BEHIND_PROXY gates it, and
server/app.py takes the last untrusted hop, not the first — sending
X-Forwarded-For: 1.2.3.4 with an incrementing address previously granted a fresh identity
per request. There are tests for exactly that spoof.
Browser origins are allow-listed. A request carrying an Origin this service does not
recognise is rejected, which is the check the MCP spec requires: a locally-bound server is
otherwise reachable from any web page the user happens to be visiting. With
STRATIFY_ALLOWED_ORIGINS unset the list is not empty — it still contains the three
claude.ai hosts in DEFAULT_BROWSER_ORIGINS. If you self-host and do not want those,
edit that constant; setting the environment variable only adds to it.
The three settings most likely to be got wrong:
- Do not publish the service port. Docker publishes to
0.0.0.0by default and installs its iptables rules ahead ofufw. Bind to127.0.0.1and terminate TLS in front of it. ClickHouse should have no host port mapping at all. - If you enable URL-embedded keys, make sure your proxy does not log them.
POST /mcp/k/{key}exists because some MCP clients cannot send a custom header, and it puts an API key somewhere keys should not be: the request line, which almost every reverse proxy logs by default. The hosted service suppresses logging for that path. A fresh deployment does not. Either configure the same suppression or leave the route off — it is gated byALLOW_URL_KEYand returns 404 when disabled. - Do not give the service the ClickHouse admin user. The
defaultuser holdsaccess_managementplusFILE,URL,S3andREMOTE. The service never needs it — every request binds a per-tier user.docker-compose.example.ymlblanksCLICKHOUSE_PASSWORDfor this reason.