Back to the catalog

13FLOW

Source-linked SEC 13F research, quality checks, watchlists and audit trails for agents.

Open source Repository Open in the app JSON README (API)

About

Source-linked SEC 13F research, quality checks, watchlists and audit trails for agents.

Details

Kind
MCP servers
Topic
No topic detected
Publisher
bluetouff
Origin
official
Category
ferramentas
Transport
http
Version
1.0.0
Stars
3
Last push
2026-09-04T08:28:50Z
Repository state
ativo
Language
Python
License
AGPL-3.0
Added
2026-08-29 03:02:31
Updated
2026-08-29 03:02:31
Origin id
io.github.bluetouff/13flow

README

# SmartMoney — 13F superinvestor tracker

Reconstructs hedge-fund / superinvestor portfolios straight from **SEC EDGAR**
13F-HR filings, and diffs quarter-over-quarter to surface new positions, exits,
adds and trims. Data is U.S.-government public domain — free to use and redistribute.

**Guides:** [`TEST_LOCAL.md`](TEST_LOCAL.md) to run it on your machine in minutes ·
[`INSTALL_SERVER.md`](INSTALL_SERVER.md) for production deployment · [`SECURITY.md`](SECURITY.md)
for the threat model and audit.

## Setup
```bash
pip install -r requirements.txt   # or: pip install requests defusedxml flask
export SEC_UA="SmartMoney/1.0 you@example.com"   # SEC requires a contact email or 403s you
export OPENFIGI_APIKEY="..."                       # optional, free; lifts FIGI rate limits
python run.py --list                 # tracked funds
python run.py --verify               # sanity-check seed CIKs against EDGAR
python run.py --fund "Berkshire Hathaway" --top 15 --enrich   # --enrich adds tickers
```
Offline tests (no network):
```bash
pip install pytest
python -m pytest tests/ -q     # offline suites: parsing, figi, db, valuation, alerts, resolver, security, Pro API
```

## CUSIP resolution & the long tail
13F rows carry only CUSIPs. `resolver.py` runs a confidence-ranked chain so the tail that
OpenFIGI misses still gets resolved where possible, with provenance recorded:
```
manual override (1.0) → OpenFIGI (0.95) → CUSIP issuer-prefix (0.65–0.85)
→ SEC name match (0.60) → unresolved (0.0, issuer name kept)
```
The CUSIP-prefix step reuses a confident sibling's ticker for a different share class/unit of
the same issuer; the SEC step matches `nameOfIssuer` against SEC `company_tickers.json`. Misses
are cached with a timestamp and re-tried after a TTL (the tail shrinks as data improves), not
forever. `--sync --enrich` uses the full chain.
```bash
python run.py --coverage                 # % of 13F value resolved, per fund + the worst tail
python run.py --resolve-sweep            # re-run the chain over unresolved CUSIPs, back-fill
```
Provenance + confidence are stored per holding, so the dashboard's reconcile dot and the
valuation reconcile ratio can flag weak mappings. Optional `cusip_overrides.json` (`{"<cusip>":"TKR"}`)
hard-maps stubborn names at top confidence.

## Dashboard (web UI)
A read-only Flask API exposes the store as JSON, and a single-file dashboard consumes it.
```bash
pip install flask
python -m smartmoney.api --db smartmoney.db            # serves http://localhost:5000
python -m smartmoney.api --db smartmoney.db --value     # also enable live valuation (stooq)
```
Open `http://localhost:5000`. The UI is branded **13FLOW** (dark editorial theme — emerald =
institutions, amber = insiders, converging like the Confluence score) with five screens:
**Consensus** (who's buying / most owned), **Funds** (holdings, current weights, implied P&L,
conviction sparklines), **Compare** (overlap matrix across funds), **Alerts** (subscriptions +
the diff feed), and **Confluence** (13F accumulation × insider buying). A served **FAQ** page
(`/faq`) explains the product, linked from the sidebar. Demo data is available only when
explicitly requested (`?demo=1` in the browser or `SMARTMONEY_CONFLUENCE_DEMO=1` for the
Confluence API); production errors are shown instead of silently substituting samples. API endpoints:
`/api/live-status`, `/api/funds`, `/api/fund/<cik>`,
`/api/consensus/{buys,holdings}`, `/api/compare`,
`/api/signals/confluence`, `/api/signals/confluence/history`,
`/api/methodology/confluence-v1`, `/api/data-quality`, `/api/agent-stats`,
`/api/openapi.json`, and the
MCP Streamable HTTP endpoint `/api/mcp`. The Registry surface exposes only bounded, read-only
public tools. Optional Pro/x402 tools are disabled by default and are not advertised publicly.
The official Registry manifest is [`server.json`](server.json). Static,
crawler-friendly pages are
served at `/funds`, `/funds/<cik>`, `/stocks`, `/stocks/<ticker>`, `/signals`, and
`/signals/<ticker>`, with SEC links where an accession or issuer search can be resolved.
`/agents` and `/fr/agents` publish 7/30-day MCP activity from durable UTC aggregates. They
make no unique-user claim and retain no IP address, User-Agent, client version, raw client
identifier, arguments, prompts, responses or keys.
The separate operator-only `/stats/` surface is an optional password-protected GoAccess
report generated from Apache logs. It adds no browser tracking, strips query strings,
anonymizes report IPs at level 2 and keeps a 90-day reporting window. Its dedicated CSP is
scoped to the authenticated report so the public site's nonce policy remains unchanged.
Production also installs a first-in-order static `000-zen-default.conf` vhost. Unknown Host
headers receive an inert ZEN page and a separate minimal access log instead of reaching
13FLOW or contaminating `13flow_access.log`; named vhosts continue to route normally.
`/api/live-status` is the public,
machine-readable proof of live state: SHA, source (`SEC EDGAR`), latest quarter, row counts,
data-quality summary, and `uses_synthetic_data=false`. `/api/product-status` is the
machine-readable operational status surface: it states live data coverage, disabled
claims, and why full quantitative validation remains blocked until imported 2013-2026
adjusted-price and normalized Form 4 transaction artifacts are available. See the
Core V1 scope gate in [`docs/CORE_V1_BOUNDARY.md`](docs/CORE_V1_BOUNDARY.md).
Current product and research boundaries are exposed at `/status`, `/readiness`,
`/api/product-status` and `/api/research-readiness`. `/api/pro-offer` remains a retired
compatibility endpoint and is not exposed as a canonical MCP tool.

## No browser accounts or checkout
Core V1 deliberately has no browser account system, no public signup, no
self-serve checkout and no Stripe billing flow. Pro API access is operator
issued: create a scoped key, deliver the plaintext token once through an
approved secure channel, verify `/api/pro/v1/status`, and keep audit/rotation
in the Pro control plane.

## Pro API
The Pro API is an explicit, versioned API-key surface for institutional and automated use.
It is off by default. Enable it with `SMARTMONEY_PRO_API=1` and store keys, counters, and
audit events in a dedicated control-plane SQLite file via `SMARTMONEY_PRO_DB`.

Recommended production split:
```bash
# /etc/13flow/13flow-web.env, used by 13flow.service on 127.0.0.1:8000
SMARTMONEY_OPEN=1
SMARTMONEY_DB_READONLY=1
SMARTMONEY_DB=/var/lib/13flow/13flow.db
SMARTMONEY_ADMIN_PANEL_USER=admin@toonux.com
SMARTMONEY_ADMIN_SESSION_SECRET=<server-only-random-secret>
SMARTMONEY_ADMIN_PASSWORD_PBKDF2=<pbkdf2-sha256$...>
SMARTMONEY_ADMIN_SESSION_SECONDS=1800
SMARTMONEY_ADMIN_TOTP_SECRET=<base32-secret>
SMARTMONEY_ADMIN_TOTP_REQUIRED=1

# /etc/13flow/13flow-pro.env, used by 13flow-pro.service on 127.0.0.1:8001
SMARTMONEY_OPEN=1
SMARTMONEY_DB_READONLY=1
SMARTMONEY_DB=/var/lib/13flow/13flow.db
SMARTMONEY_PRO_API=1
SMARTMONEY_PRO_DB=/var/lib/13flow-pro/13flow-pro.db
SMARTMONEY_PRO_KEY_PEPPER=<server-only-random-secret>
SMARTMONEY_PRO_REQUIRE_KEY_PEPPER=1
SMARTMONEY_PRO_ACCEPT_LEGACY_SHA256_KEYS=0
```

Do not grant `/var/lib/13flow-pro` write access to the public `13flow.service`. Apache
should route only `/api/pro/` to `13flow-pro.service`, while the public site and open JSON
endpoints stay on the read-only service.

Admin panel access at `/pro/admin` is protected by a server-side admin session. Generate
the session secret and password hash on the server; the Pro API key used inside the panel
is still separate and must carry `admin:read,admin:write`.

```bash
openssl rand -hex 32
/opt/13flow/.venv/bin/python /opt/13flow/run.py --hash-admin-password
/opt/13flow/.venv/bin/python -c 'import base64,secrets; print(base64.b32encode(secrets.token_bytes(20)).decode().rstrip("="))'
```

Create an API key offline as the operator. The plaintext token is shown exactly once. In
production, only an HMAC-SHA256 hash derived with the server-only
`SMARTMONEY_PRO_KEY_PEPPER` is stored, so tokens generated from a GitHub clone or another
instance cannot authenticate against `13flow.eu`. With
`SMARTMONEY_PRO_REQUIRE_KEY_PEPPER=1`, legacy SHA-256 rows are rejected unless
`SMARTMONEY_PRO_ACCEPT_LEGACY_SHA256_KEYS=1` is explicitly set for a short migration window.
```bash
sudo -u flowpro env SMARTMONEY_PRO_KEY_PEPPER="$SMARTMONEY_PRO_KEY_PEPPER" \
  SMARTMONEY_PRO_REQUIRE_KEY_PEPPER=1 \
  /opt/13flow/.venv/bin/python /opt/13flow/run.py \
  --create-api-key "Acme Asset Management" \
  --pro-db /var/lib/13flow-pro/13flow-pro.db \
  --api-key-scopes funds:read,quality:read \
  --api-key-rate-per-min 120 \
  --api-key-rate-per-day 10000

python run.py --list-api-keys --pro-db /var/lib/13flow-pro/13flow-pro.db
python run.py --revoke-api-key <key_id> --pro-db /var/lib/13flow-pro/13flow-pro.db
python run.py --prune-pro-audit-days 180 --pro-db /var/lib/13flow-pro/13flow-pro.db
```

Use `Authorization: Bearer <token>` or `X-13FLOW-Key: <token>`.
```bash
curl -H "Authorization: Bearer $TOKEN" https://13flow.eu/api/pro/v1/status
curl -H "Authorization: Bearer $TOKEN" https://13flow.eu/api/pro/v1/funds
curl -H "Authorization: Bearer $TOKEN" https://13flow.eu/api/pro/v1/fund/0001067983
curl -H "Authorization: Bearer $TOKEN" \
  "https://13flow.eu/api/pro/v1/fund/0001067983?include_holds=0&limit_positions=20&limit_moves=50"
curl -H "Authorization: Bearer $TOKEN" https://13flow.eu/api/pro/v1/data-quality
curl https://13flow.eu/api/pro/v1/openapi.json
curl https://13flow.eu/api/product-status
curl https://13flow.eu/api/research-readiness
```

Security properties: opaque high-entropy tokens, key hashes only at rest, scoped access
(`funds:read`, `quality:read`), persistent per-minute/per-day rate limits, and an audit row
for every Pro request including denied and rate-limited calls. Pro responses are explicitly
non-cacheable (`private, no-store, max-age=0`) and vary on both supported key headers so
reverse proxies cannot mix responses across credentials.

Operational baseline:
- one active key per institution or internal service;
- revoke unused QA/bootstrap keys immediately;
- rotate institutional keys on a fixed schedule and after personnel/vendor changes;
- keep Pro audit rows long enough for incident response, then prune with
  `--prune-pro-audit-days`;
- back up `SMARTMONEY_PRO_DB` with encrypted backups only. See
  [`deploy/PRO_API_SPLIT.md`](deploy/PRO_API_SPLIT.md) and `deploy/backup-pro-db.sh`.

Before issuing or renewing a Pro key, confirm
`/api/pro/v1/admin/release-readiness` returns `go: true` with no blockers.

`GET /api/pro/v1/fund/<cik>` is the institutional detail endpoint: it returns the selected
filing metadata, previous filing metadata, full holdings, share-count moves versus the
previous quarter, fund-scoped data-quality warnings, and the methodology block needed to
reproduce the interpretation. For production integrations, use `include_holds=0`,
`limit_positions`, and `limit_moves` to keep payloads bounded while retaining the same
calculation basis. Responses include `positions_total`/`positions_returned` and
`changes_total`/`changes_returned` so clients can detect truncation deterministically.

## Alerts — real delivery
Subscribe to a fund and get the **diff** (not just "a filing appeared") delivered when a
new 13F lands. Channels: console (default), webhook, email.
```bash
python run.py --subscribe "Berkshire Hathaway"                       # console, primed
python run.py --subscribe "Scion Asset Mgmt" --channel webhook --target https://hooks.you/x
python run.py --list-subs
python run.py --alerts-run          # sync subscribed funds from EDGAR, then deliver new ones
python run.py --alerts-dispatch     # deliver pending from already-stored filings (offline)
```
Email needs `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_FROM` in the env.
Key properties: idempotent (a crash/restart never re-sends or drops — the deliveries table is
the boundary), new subscribers are **primed** so they only get future filings, failed sends are
recorded and retried next run, and subscribing is gated to the paid tier. Drive `--alerts-run`
from cron rather than the in-process `poll()` loop.

## Confluence (13FLOW) — where 13F accumulation meets insider buying
A separate screen ranks tickers where **superinvestor 13F accumulation** and **open-market
Form 4 insider buying** coincide — a rare, hard-to-fake overlap. It reuses the existing EDGAR
etiquette (UA + rate limit), the `defusedxml` hardening, and the dashboard theme.
```
GET /api/signals/confluence?window=90&min_score=0
→ { "kpis": {...}, "signals": [ {ticker, score, quadrant, breakdown{...}, institutional{...}, insider{...}}, ... ] }
GET /api/methodology/confluence-v1
→ frozen score version, parameters, universe, split, parameter_hash
GET /api/signals/confluence/history?ticker=TSM&window=90
→ append-only signal revisions from confluence-history.jsonl
```
The 0–100 score is an **ordinal exploratory ranking**, not a probability, not a historical
frequency, and not an expected-return estimate. `FeatureParams` controls *what the signal
measures* (recency half-life, buy-size curve, cluster window, seniority) and remains a set of
judgement parameters until sensitivity tables are published. `Weights` controls *how the
pillars combine* (institutional breadth, insider conviction, recency-weighted dollars,
agreement bonus, minus trim/sell penalties). The default weights are **heuristic**; the
current live build must be treated as **not calibrated on live historical outcomes** and not
validated out-of-sample. Each signal exposes its per-pillar `breakdown`
and a quadrant label (Conviction / Institutional bid / Insider conviction / Distribution /
Divergent / Neutral), but the quadrant describes direction while the score describes heuristic
intensity, so they can diverge.

Data-scope boundary: 13F filings are delayed long-US-securities disclosures, not complete
fund portfolios. They omit shorts, most non-US holdings, bonds, full derivative books,
intra-quarter trading and confidential-treatment omissions. The live Confluence rail also
uses a bounded Form 4 issuer universe driven by tracked 13F activity; insider-only and
distribution quadrants are therefore not exhaustive. Current Form 4 parsing focuses on
normalized Table I transactions; Table II derivatives, 10b5-1 plan flags, multi-owner
attribution and weighted-average price footnotes remain explicit limitations until modeled.

The quantitative proof boundary is public: `VALIDATION_PROTOCOL.md` defines the required
point-in-time dataset, train/validation/test split, baselines, neutralization, costs,
confidence intervals, permutation tests, and version log before any score can be called
validated. Until then, the correct wording is: **backtest harness available; default weights
are heuristic**. The offline price export, dataset builder and publication gate are:

```bash
python run.py \
  --build-validation-prices \
  --validation-tickers /var/lib/13flow/validation_tickers_sample25.txt \
  --validation-prices-out /var/lib/13flow/validation_prices_sample25.csv \
  --validation-price-provider massive \
  --validation-start 2013-01-01 \
  --validation-end 2026-07-02 \
  --validation-price-sleep-sec 15 \
  --validation-price-retry-attempts 8 \
  --validation-price-retry-base-sec 60 \
  --validation-price-retry-max-sec 900 \
  --validation-price-timeout-sec 10 \
  --validation-json

python run.py --db /var/lib/13flow/13flow.db \
  --build-validation-dataset /var/lib/13flow/confluence_features.csv \
  --validation-prices /var/lib/13flow/validation_prices_sample25.csv \
  --validation-form4 /var/lib/13flow/validation_form4_sample25.csv \
  --validation-tickers /var/lib/13flow/validation_tickers_sample25.txt \
  --validation-code-commit "$SHA" \
  --validation-json

python run.py --validation-dataset /path/to/confluence_features.csv --validation-json
```

The price exporter writes a provider-neutral `ticker,date,adj_close` CSV and reuses
already exported ticker rows unless `--validation-price-force` is set. Massive requires
`MASSIVE_API_KEY` in the process environment; `stooq` is available as a free fallback for
operator smoke tests, and `yahoo` is a no-key research fallback when a vendor account cannot
serve enough history. Any non-Massive fallback must be disclosed in the validation artifact
as a research price source, not an institutional production feed. The exporter retries `429`
and `5xx` responses with exponential backoff, honors `Retry-After`, deduplicates resumed rows
and reports complete/partial history coverage per ticker. It checkpoints the CSV after each
ticker so interrupted runs remain resumable. Use `--validation-price-max-tickers 1` for first
contact with any new or fallback provider. Passing the same
`--validation-tickers` file to the dataset builder filters the feature export to that priced
universe; omit it only when the price CSV covers the full validation universe. The dataset
gate returns the feature-table SHA256, split counts, schema gaps, version mismatches and rank
metrics for the score plus available baselines.

Imported vendor/bulk price files can be checked without touching any external API:

```bash
python run.py \
  --validate-price-csv /var/lib/13flow/validation_prices_full.csv \
  --validation-tickers /var/lib/13flow/validation_tickers_priceable.txt \
  --validation-start 2013-01-01 \
  --validation-end 2026-07-02 \
  --validation-json
```

The validator reports required columns, positive-price failures, duplicate ticker/date rows,
missing tickers, partial histories and major calendar gaps before the file is used in a
validation dataset.

The Form 4 validation artifact can be produced from SEC EDGAR with a conservative,
checkpointed exporter:

```bash
SEC_UA='13FLOW/1.0 contact@example.com' python run.py \
  --build-validation-form4 \
  --validation-tickers /var/lib/13flow/validation_tickers_sample25.txt \
  --validation-form4-out /var/lib/13flow/validation_form4_sample25.csv \
  --validation-start 2024-07-03 \
  --validation-end 2026-07-02 \
  --validation-form4-sleep-sec 2 \
  --validation-form4-max-tickers 1 \
  --validation-json
```

Increase `--validation-form4-max-tickers` only after the one-ticker smoke succeeds. The
exporter reuses existing ticker rows unless `--validation-form4-force` is set and caps each
ticker with `--validation-form4-max-filings-per-ticker`.

For the operator checklist after a long Form 4 export finishes, including the offline CSV
gate and deployment order, see `docs/POST_RUN_FORM4_VALIDATION.md`.

The dataset builder joins the reviewed local Form 4 transaction file with
`--validation-form4`. Accepted CSV/JSONL rows include `ticker`, `accession`,
`filing_date`, `transaction_date`, owner identity/role fields, transaction code,
acquired/disposed flag, shares, price and ownership-after fields. The join is point-in-time:
only Form 4 filings accepted by the dataset `as_of` date and transactions inside the trailing
window enter the row. Without that file the builder still exports
`feature_scope=13f_only_no_form4`; with it the scope becomes `13f_form4_joined`. Neither scope
is a full validation claim until the imported price and Form 4 artifacts, coverage, costs and
no-lookahead controls are reviewed. Non-priceable/common equity suspects are excluded by
default; use `--validation-include-non-priceable` only for auditing noisy 13F rows.

Confluence v1 is frozen as a machine-readable research contract in
`docs/confluence_v1.json` and documented in `docs/CONFLUENCE_V1.md`. The append-only signal
history is written to `confluence-history.jsonl` in `SMARTMONEY_CACHE_DIR`; corrections are
new revisions, not in-place edits.

The production live provider also has an explicit effective universe: to keep the public tier
off abusive Form 4 fan-out, it scans insider filings only for tickers with at least
`SMARTMONEY_CONFLUENCE_SCAN_MIN_FUNDS` tracked funds opening or adding in the latest 13F
quarter (default 3). Trim/exits are computed more broadly, but insider-only, distribution,
and divergent categories are therefore not exhaustive in this production path. This is
exposed in `/api/signals/confluence` under `metadata.effective_universe`.

The screen lives as a fifth dashboard tab. Production must use either a precomputed
`confluence-<window>.json` cache or the live provider. The live provider needs EDGAR access
for Form 4s:
```bash
SEC_UA="you@example.com" SMARTMONEY_CONFLUENCE_LIVE=1 python -m smartmoney.api --db demo.db
```
With no cache and no live provider, the endpoint returns `503 confluence_unavailable`. Use
`SMARTMONEY_CONFLUENCE_DEMO=1` only for explicit local demos. Evaluate hypotheses or fit
research weights with `python -m smartmoney.backtest` (synthetic demo only) — see
`FORMS4_INTEGRATION.md` and `VALIDATION_PROTOCOL.md` for the feature write-up and validation
contract.

Offline research/admin helpers:
```bash
python run.py --freeze-confluence-v1 docs/confluence_v1.json
python run.py --append-signal-history --cache-dir /var/lib/13flow --confluence-windows 30,90,180
```

## Valuation — current weights & implied P&L
A 13F reports value at *quarter-end*. To see what the book is worth *now* and the paper
P&L since the filing, revalue stored holdings at live prices:
```bash
python run.py --value "Berkshire Hathaway"                       # stooq (free, default)
python run.py --value "Berkshire Hathaway" --provider massive --fundamentals
python run.py --value "Berkshire Hathaway" --basis 2024-09-30    # value a specific quarter
```
Prices are pluggable: **stooq** (free, no key) by default; **massive** (Massive Market Data,
Polygon-shaped) with `MASSIVE_API_KEY` set, which also yields market cap and % of company owned.
Each priced line shows a **reconcile ratio** (reported ÷ shares×quarter-end-close): ~1.00×
means the CUSIP→ticker map is right; far from 1 flags a bad mapping. P&L is *paper* — it
assumes holdings are unchanged since the filing, which the 45-day lag guarantees they're not.

## Persistence & cross-fund screens
Snapshots are stored in SQLite so diffs and multi-fund questions become queries.
```bash
python run.py --sync "Berkshire Hathaway" --enrich      # backfill one fund
python run.py --sync-all --max-quarters 12 --enrich     # backfill everything
python run.py --buys 2024-12-31 --min-funds 3           # who's BUYING (diff-based)
python run.py --consensus 2024-12-31 --min-funds 3      # who's HOLDING (pure SQL)
python run.py --quality --db smartmoney.db              # DB-only data-quality warnings
python run.py --preflight --db smartmoney.db            # DB-only production readiness checks
python run.py --timeline "Berkshire Hathaway" --cusip 037833100   # conviction over time
```
`--sync` only fetches filings not already stored, so re-runs are cheap and pick up just
the newest quarter. The `--buys`/`--consensus`/`--timeline` screens are pure DB reads —
no SEC_UA needed once data is synced.

## Operator preflight
`run.py --preflight` is an offline release gate. It never calls EDGAR. It checks deploy SHA
traceability, opens the market DB read-only, verifies the `latest_filings` view has content,
summarizes data quality, verifies the Pro DB is writable, checks active Pro keys and recent
audit rows, and, when a token is provided via environment, validates the Pro API contract
in-process without putting the token in shell history. In rsync-style deployments without a
`.git` directory, the CLI reads the deployed SHA from the systemd drop-in
`/etc/systemd/system/13flow.service.d/version.conf`.

```bash
SHA=<deployed-git-sha>

printf "API token: "
read -r -s SMARTMONEY_PRO_TOKEN
printf "\n"
export SMARTMONEY_PRO_TOKEN

sudo -E /opt/13flow/.venv/bin/python /opt/13flow/run.py --preflight \
  --db /var/lib/13flow/13flow.db \
  --pro-db /var/lib/13flow-pro/13flow-pro.db \
  --require-pro \
  --expected-sha "$SHA"

unset SMARTMONEY_PRO_TOKEN

sudo -E /opt/13flow/.venv/bin/python /opt/13flow/run.py --preflight --preflight-json ...
```

## Public smoke test
`deploy/smoke-public.sh` is the crawler-visible release gate. It makes live HTTP calls to the
public site or staging, but never calls EDGAR. It fails if the root page regresses to
`SAMPLE DATA`, auth/checkout copy appears in the open build, FAQ/Legal show legacy text,
legacy page aliases stop redirecting to canonical URLs, public JSON contracts break, MCP
disappears, or a private MCP tool is exposed on the default public surface.

```bash
EXPECTED_SHA=<deployed-git-sha> /opt/13flow/deploy/smoke-public.sh
SITE=https://staging.13flow.eu EXPECTED_SHA=<sha> /opt/13flow/deploy/smoke-public.sh
```

The public smoke now prints a timing summary and slow checks so a longer release gate can be
distinguished from slower production routes. For a focused latency guard, run the lightweight
public perf smoke after the functional smoke:

```bash
/opt/13flow/deploy/perf-smoke-public.sh
SAMPLES=7 WARN_MS=1500 FAIL_MS=3000 /opt/13flow/deploy/perf-smoke-public.sh
```

## Open build (public, read-only — no auth, no Stripe, no browser alerts)
There is a first-class **open mode** for a public deployment that exposes only the read-only
screens (Consensus / Funds / Compare / Confluence) with no accounts and no payment.
`--open` is kept for compatibility; Core V1 always registers only the public/Pro controlled
pilot surface:
```bash
SMARTMONEY_OPEN=1 SMARTMONEY_DB_READONLY=1 python -m smartmoney.api --db demo.db
# or: python -m smartmoney.api --db demo.db --open --readonly
```
Core V1 does not register browser auth, billing or subscription routes
(`/api/auth/*`, `/api/billing/*`, `/api/subscriptions`, `/api/alerts/*` return
404, not 401). `SMARTMONEY_DB_READONLY=1` opens SQLite read-only so the public web process
cannot write the market database, and the dashboard auto-detects the build via `/api/config`.
The Pro API is separate: run it in the dedicated `13flow-pro.service` with
`SMARTMONEY_PRO_API=1`; `/api/pro/v1/*` writes only to `SMARTMONEY_PRO_DB`, not to the
read-only 13F data DB, and the public `13flow.service` should keep no Pro DB write path. A complete
**Debian + Apache** deployment kit (gunicorn systemd unit with a sandbox,
Apache TLS reverse-proxy vhost with a GET-only method allow-list + HSTS/CSP, an ingest user
separated from the web user, and a scheduled refresh) lives in [`deploy/`](deploy/) — see
[`deploy/INSTALL_DEBIAN_APACHE.md`](deploy/INSTALL_DEBIAN_APACHE.md).

## Architecture
- `edgar.py` — rate-limited client (8 req/s, under SEC's 10/s ceiling), CIK resolution,
  submissions feed, locates + downloads the holdings XML.
- `parser.py` — namespace-agnostic info-table parser → raw holdings.
- `portfolio.py` — aggregates rows to one line per (CUSIP, put/call), normalizes value units, weights.
- `figi.py` — **CUSIP → ticker** via OpenFIGI v3: batched, rate-limited, 429-aware,
  no-exchCode fallback, persistent disk cache.
- `resolver.py` — long-tail resolver chain (OpenFIGI → CUSIP-prefix → SEC name → manual),
  confidence + provenance, retryable cache, coverage reporting.
- `diff.py` — classifies moves by **share count**: NEW / EXIT / ADD / TRIM / HOLD.
- `db.py` — **SQLite store**: save/load portfolios, a `latest_filings` view so amendments
  supersede, and SQL screens (consensus holdings, conviction timeline, holders, AUM timeline).
- `analytics.py` — **consensus buys/sells** across funds (diff-based, the sharper screen).
- `prices.py` — pluggable price/fundamentals providers: `StooqProvider` (free) + `MassiveProvider`.
- `valuation.py` — revalue a stored portfolio at current prices: current weights, implied
  P&L since quarter-end, reconcile check, % of company owned.
- `registry.py` — superinvestor seed list (CIK is the stable key).
- `tracker.py` — wires it together: `sync_fund` ingestion + freemium gating (free = 3 funds).
- `channels.py` — delivery channels: console / webhook / email (+ callable for tests).
- `alerts.py` — `AlertEngine`: diff-carrying alerts, persistent dedup, priming, paid-tier gate.
- `api.py` — controlled-pilot Flask JSON API over the store; serves public pages and Pro API routes.
- `netsec.py` — egress safety: SSRF guard for webhook URLs + email-recipient validation.
- `pro.py` — Pro API keys, scopes, persistent rate limits, and request audit.
- `forms4.py` — Form 4 discovery by issuer CIK + ownership-XML parser (open-market P/S), XXE-hardened.
- `crosssignal.py` — Confluence engine: 13F accumulation × insider buying → scored, classified signal.
- `backtest.py` — rank-IC / quantile-spread harness + coordinate-ascent research optimiser.
- `api_signals.py` — read-only `GET /api/signals/confluence` blueprint (live + sample providers).
- `dashboard.html` — single-file research app source; served at `/app`, with `/dashboard.html` redirecting there.
- `faq.html` — branded FAQ / explainer source, served at `/faq`; `/faq.html` redirects there.

See **`SECURITY.md`** for the threat model, the audit findings, and deployment hardening.

## Gotchas this code already handles (and the ones it doesn't)
**Handled:**
- Mandatory `User-Agent` + 10 req/s limit on EDGAR.
- **Value units changed in 2023**: pre-2023-01-03 `<value>` is in *thousands*, after it's
  *whole dollars*. Normalized by report date in `portfolio.py`.
- Multiple rows per issuer aggregated; puts/calls kept distinct from long stock.
- Amendments (13F-HR/A) skipped for the headline diff (they restate, not re-trade).
- **CUSIP → ticker** via OpenFIGI: batch 100 jobs/req with key (5 without), v3 `warning`
  = no-match handled, 429 backoff, results cached to disk so steady-state cost ≈ only new CUSIPs.

**Not handled yet (the real work ahead):**
- **No-match CUSIPs.** OpenFIGI resolves the vast majority of 13(f) securities, but expect a
  long tail (some bonds, units, recently-issued names) to come back empty — they're cached as
  misses and worth a periodic re-sweep.
- **The 45-day lag.** 13F is filed up to 45 days after quarter-end, so no alert is ever
  trade-fresh — the edge is being first to the *filing event* and to a clean diff, not to price.
- Confidential-treatment requests can delay/omit positions; backfill when the amendment lands.
- Pre-2013 filings are plain-text tables, not XML — this parser targets the XML era.

## Roadmap toward the product
1. ✅ CUSIP→ticker enrichment (OpenFIGI) — `figi.py`. Next: join price/market-cap.
2. ✅ Persistence (SQLite) + cross-fund screens — `db.py` / `analytics.py`.
3. ✅ Price / market-cap join — `prices.py` / `valuation.py` (current weights, implied P&L).
4. ✅ Real alert delivery — `alerts.py` / `channels.py` (diff payload, dedup, channels, paywall).
5. Freemium server-side: gating logic lives in `tracker.Tier` (fund limit + alerts flag).
6. ✅ UX layer — `api.py` + `dashboard.html` (consensus, fund pages, compare, alert feed).

When you outgrow SQLite, the swap to Postgres is mechanical: the schema uses standard
window functions + one view, and the repository is plain SQL (no ORM lock-in).

## License

13FLOW is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0-or-later).
If you run a modified version on a network server, you must make the complete source
available to its users. See [LICENSE](LICENSE).

Data: SEC EDGAR (US public domain). 13FLOW is an analysis screen, not investment advice.

More