Change-detection over the live SEC EDGAR filing stream. Maintain a watchlist (CIKs/tickers) and form-type filter (8-K, 4, SC 13D/13G, S-1, 10-K/10-Q); each run emits one record per newly-appeared accession, joined to issuer metadata and event-classified. Keyless. Not endorsed by the SEC.
Added the store-listing + pricing draft docs, mirroring the openfema-monitordocs/ shape
(draft-for-owner-review banner, no-publish gate reminder, launch checklists).
docs/LISTING.md — Store copy: name/title from .actor/actor.json
(sec-edgar-filing-monitor), short + full description, categories/SEO, output-record shape,
example input, and the required SEC no-endorsement disclaimer. Customer promise = normalized,
issuer-joined records + first-seen eventType classification (ownership-stake / insider-trade
/ material-event / filing, verified against src/filters.js:158-166). Mandatory-UA / ≤10 req/s
/ EFTS 2001+ + 10k-hit windowing kept as internal maintenance notes, not customer caveats.
docs/PRICING.md — PPE model: single filing-returned event, one charge per filing
(src/main.js:254); _noData (main.js:244) / _error (main.js:280) control records
uncharged; charge-cap eventChargeLimitReached handling (main.js:255) noted. Intended band
≈ $0.001/filing per BUILD_QUEUE.md §5 documented as framing only — price VALUE is
owner-deferred (TODO placeholder), rejected alternatives (per-run, tiered, per-CU) recorded.
Docs only; no source or test changes (prior node --test still 58 pass, untouched).
0.1 — 2026-07-10 — slice 5: COMPLIANCE.md
Added the compliance gate doc, mirroring the openfema-monitorCOMPLIANCE.md shape (verdict →
data source & legal status → robots.txt → access terms/rate limits → summary table). Cites
BUILD_QUEUE.md §5 as the source of record (license, robots, fair-access terms verbatim; evidence
captured 2026-07-10 live, not re-fetched here — this build is offline).
License: U.S. public domain (17 U.S.C. §105); redistributable without permission, commercial
use permitted; SEC citation requested, not required — satisfied via the emitted no-endorsement
disclaimer (the ATTRIBUTION const in src/edgar.js, on every record).
Ported the slice-3 offline smoke harness into committed test files mirroring the openfema-monitor
slice-4 layout (Node's built-in runner, no extra deps, NO network — the HTTP layer is exercised
through injected fetchImpl/fetchPage/sleep/now). 58 tests, all pass.
test/edgar.test.js — the pure layer: zeroPadCik/unpadCik (pad/strip, CIK-prefix +
separator tolerance, overflow → null), toEdgarDate (date/datetime, range validation),
buildPrimaryDocumentUrl, buildSubmissionsUrl, buildEftsUrl (q→*, comma forms/ciks,
padded+deduped CIKs, encoded phrase, date window, from/hits — decoded back via URL),
buildTickerMap/resolveWatchlist (unknown-ticker callback), parseDisplayName (CIK group never
a ticker), and both normalizers — normalizeEftsHits (_id-derived adsh + primaryDoc, wrapper /
bare-array inputs, extra fields) and normalizeSubmissions (columnar index-alignment, short-array
→ null, no-accession → []).
test/fetch.test.js — the HTTP layer: backoffDelayMs/isRetryableStatus (403 non-retryable),
fetchJson (mandated UA + Accept, dispatcher passthrough, 5xx/429 retry + backoff schedule, 400/403
give-up carrying .status/.body, transport-error retries, fair-access throttle via injected clock),
and fetchAllHits (from/hits paging, maxResults cap, over-ceiling multi-day window split at the
midpoint, unsplittable single-day window truncation → truncatedWindows, dep forwarding).
test/filters.test.js — all six pure helpers, esp. filterByForm/A-amendment family match,
filterNewOnly (acceptanceDateTime-vs-filingDate cursor, strictly-after, cursorless kept but no HWM
move, non-regression), and classifyEvent's four classes incl. the 424B guard (must NOT be
insider-trade — the 4 match is ===, not a prefix).
No main.js import anywhere (it runs Actor.init() on import); no source changes — tests only.
Implemented the pure filter/classify layer and the Actor runtime (all slice-1 stubs filled; no new
deps; no network in verification).
filters.js (pure, order-preserving, null-tolerant):dedupeByAdsh (keep-first per
accession; keyless kept as-is), filterByForm (match form OR rootForm, case-insensitive,
/A-amendment family match so 8-K/A is kept under an 8-K filter; empty = all; both-null
dropped when active), filterByCik (restrict to the resolved 10-digit watchlist; empty = all),
filterNewOnly(records, highWaterMark) (first-seen HWM — cursor = acceptanceDateTime else
filingDate fallback; strictly-after kept + tagged isNew; cursorless kept but never advance the
HWM; returns { records, highWaterMark }, never regresses below the input), classifyEvent
(ownership-stake SC 13D/13G · insider-trade Form 4/4A · material-event 8-K · else filing),
applyMaxResults (0 = uncapped).
main.js (Actor wiring, filing-returned PPE): resolve watchlist (fetch company_tickers.json
once, only when tickers given; warn on unknown tickers) → feed-path choice (EFTS full-text when
a query is given / watchlist empty / watchlist > 50 CIKs; else the per-issuer submissions feed —
exact acceptanceDateTime, ideal for monitorNewOnly; logged) → date window (HWM date part
overrides since under monitorNewOnly; else since or a 7-day trailing default; optional
until) → fetch (fetchAllHits windowed, or one submissions request per CIK + client-side date
window) → normalize → dedupeByAdsh → filterByForm → filterByCik → filterNewOnly →
classifyEvent → applyMaxResults → push one item per filing charging one filing-returned PPE
event each, attaching attribution + fetchedAt + eventType/isNew. _noData/_error control
records pushed but NOT charged. HWM persisted (HIGH_WATER_MARK KV key) only after a fully
successful, uncapped pass (held back on charge-limit / maxResults truncation so the tail is
re-fetched). Optional Apify Proxy → undici dispatcher (best-effort).
Decision (HWM cursor): the EFTS feed carries no acceptance datetime, so filterNewOnly falls
back to filingDate (date-granular) there; documented the same-day edge (may under-report / re-emit
once on a date-only HWM). The submissions feed (exact datetime) is the preferred monitorNewOnly
path and is auto-selected for a bounded CIK watchlist with no query.
Verified offline:node --check clean on both files + a 31-assertion offline smoke harness
(mocked normalizer output, no network) covering the full pipeline — dedupe, form/CIK narrowing,
amendment family match, all four event classes, datetime-vs-filingDate HWM (strictly-after, fallback,
cursorless, non-regression), cap, and both edgar.js normalizers feeding the filters. No committed
tests yet (that's slice 4).
0.1 — 2026-07-10 — slice 2: src/edgar.js API layer
Implemented the SEC EDGAR network + raw-shape module (all slice-1 stubs filled; no new deps).
Value coercion:zeroPadCik (bare/padded/CIK-prefixed → 10-digit, >10 digits or
non-numeric → null), unpadCik (strip pad for the Archives /data/{cik}/ path segment),
toEdgarDate (YYYY-MM-DD or full ISO → YYYY-MM-DD, month/day validated), and
buildPrimaryDocumentUrl (CIK + accession + doc → www.sec.gov/Archives/edgar/data/…).
HTTP layer (injectable):fetchJson always sends the mandated USER_AGENT, enforces a
shared module-level fair-access throttle (MIN_REQUEST_INTERVAL_MS, ≤10 req/s), and retries
5xx/429/transport with deterministic exponential backoffDelayMs. A 403 is treated as a
rejected User-Agent (non-retryable config error) and annotated in the thrown message.
sleep/now/fetchImpl/onRetry/dispatcher (proxy) are injectable; resetThrottle test hook.
EFTS paging:fetchAllHits date-windows under the 10,000-hit ceiling — probes
hits.total.value, halves any over-ceiling splittable window (newest-first), pages the rest via
from/hits, honors maxResults, and reports unsplittable over-ceiling windows in
truncatedWindows.
Normalizers (side-effect-free):normalizeEftsHits folds EFTS hits.hits[]._source ROW
objects (parses display_names → issuer name + tickers, builds primaryDocumentUrl; EFTS has no
acceptance datetime / SIC description → null) and normalizeSubmissions folds the per-issuer
filings.recentcolumnar (index-aligned) arrays joined to top-level issuer metadata. Both
honor caller-requested extra fields.
Verified offline:node --check clean + an offline smoke harness (mocked fetchImpl/clock, no
network) asserting CIK/date coercion, both URL builders, UA + throttle + 5xx-retry + 403-give-up,
ticker resolution, both normalizers, and EFTS paging incl. maxResults cap + date-window split.
0.1 — 2026-07-10 — slice 1: scaffold
Scaffolded from the openfema-monitor / usaspending-monitor / usgs-water-monitor reference
implementations (structure, patterns, PPE model). Actor #5 of the offline BUILD_QUEUE.md (§5 SEC
EDGAR).
Fetch-based Apify Actor (no browser): apify@^3.2.6 only, apify/actor-node:20 base, 256–512 MB.
.actor/: actor.json (name sec-edgar-filing-monitor, title "SEC EDGAR Filing Monitor",
intended filing-returned PPE), Dockerfile, and input_schema.json defining the product input
surface — ciks, tickers, forms, since, until, query, monitorNewOnly, maxResults,
proxyConfiguration.
src/ stubs establishing the module layout (each function throws "not implemented (slice N)"):
main.js (Actor entry, filing-returned PPE — impl slice 3).
Mandatory User-Agent baked in as a scaffold constant + documented in the README maintenance
notes: ByteHaven Studios EDGAR Monitor developer@bytehavenstudios.com (SEC fair-access; undeclared
tools rejected; ≤10 req/s). Trademark caveat noted: text reference to EDGAR is fine, SEC seal /
EDGAR marks NOT used in the product name; /cgi-bin is robots-disallowed and unused.
README.md documents the intended product surface + output record shape + the required SEC
no-endorsement attribution string; CHANGELOG.md (this file).
Verified offline: all src/*.jsnode --check clean (no network).