Skip to content
Back to Projects

REIT Comps Extraction Agent

Comps extraction from SEC 10-Ks and earnings supplementals where every value is cited verbatim, validated programmatically and hand-tied to the filing.

Shipping
Finance AI Extraction Governance · Python OpenAI API SEC EDGAR pytest
14/21 Cells Tied by Hand
6 Not Reported
1 Flagged, Not Guessed
21/21 Stable Across Runs
99 Tests
3 REITs, 3 Property Types

Summary

A pipeline that pulls the standard REIT coverage metrics (FFO per share, same-store growth, occupancy, lease term, leverage) out of SEC 10-Ks and earnings supplementals for three REITs with deliberately different property types: Prologis (industrial), American Tower (towers) and Realty Income (net lease). In investment banking terms it is the extraction layer behind the comps page of a pitch book, the raw material an analyst spends hours pulling from filings by hand.

The point is not that a model can read a filing. It is what happens to a number after the model reads it. Every value carries a verbatim anchor quote and a citation down to the character offset in the source document, passes a programmatic validation gate, and was then checked by hand against the filing on EDGAR. Every cell in the output carries one of four statuses, and a cell is never blank.

The Problem

REIT metrics are the messiest common extraction target in equity coverage. FFO and same-store NOI are non-GAAP and usually live in earnings supplementals rather than the 10-K, sometimes as image-rendered slide decks. Occupancy and lease terms appear as prose in the properties section. Different property types report different metrics entirely: a tower company has no occupancy in the usual sense, and a net-lease REIT does not report same-store NOI because its tenants pay the operating expenses.

A pipeline that fills every cell with a confident-looking number is worse than one that fills nothing, because a wrong comps figure looks identical to a right one until someone checks. So the design question was how to make “unverified” visible instead of invisible.

Approach

Ingestion. Filings are fetched from EDGAR by CIK, cached, and content-checked (size, throttle-page detection, registrant name present) before they count. Every EX-99 exhibit on the Q4 earnings 8-K is stored rather than guessing which one is the supplemental. That rule paid for itself immediately: Prologis files its supplemental as EX-99.1 and its press release as EX-99.2, the reverse of the usual convention.

Extraction. HTML is converted to text once, deterministically, and that text file becomes the single source of truth; hidden text layers inside image slide decks survive the conversion. A keyword search finds candidate windows per metric, and only those windows go to the model, which must return a verbatim quote copied from the window along with the value. The model never sees a whole document.

Four statuses, assigned by code, not by the model. CITED: the quote exists verbatim in the source text and the printed value appears inside it. TIED: cited and then confirmed by a person against the filing on EDGAR. NOT TIED: the pipeline produced something it could not tie to a source, rendered as a flag, never as a bare number. N/R: the company does not report the metric, which is a first-class outcome rather than a failure, and is assigned only after every document for that company has been searched with the metric’s full synonym list.

Validation. A standalone pass re-checks every cited value against the persisted text, re-runs extraction a second time and marks any value that differs across runs as unstable, and was demonstrated on real data by poisoning a value (5.81 to 5.18) and watching it go NOT TIED. A validator that never fires proves nothing, so the demonstration is in the README.

Hand verification. All 14 cited values were checked against the filings on EDGAR, one at a time, working from a checklist ordered by how many numbers the anchor quote spans (a quote spanning 93 numbers is a whole reconciliation table, where a wrong-year figure can sit while remaining fully cited). Each check is recorded with who checked it, when, and a note naming the line item and period column.

What It Found

A citation gate proves provenance, not period selection. On the first run, Realty Income’s AFFO per share came back as 4.19; on the second, 4.28. Both passed the citation gate, because both numbers sit in the same flattened table row (Q4 2025, Q4 2024, FY2025, FY2024), and “the value appears inside the quote” is true for either. That is the honest limit of programmatic citation checking: it can certify where a number came from and still certify the wrong column. It is why the hand-verification step exists and why the checklist is ordered by numeric density.

The pipeline surfaced two label mismatches and refused to resolve them. Realty Income’s “same-store” figure is same-store rental revenue, not NOI; Prologis reports Debt to Adjusted EBITDA on gross debt where the other two report net. Both were flagged with the label as printed in the source. Deciding whether to accept them as analogs was a human call, recorded in DECISIONS.md with the reasoning, and both are footnoted in the table. During hand verification the checker’s own shorthand described the Prologis figure as “net debt,” and the record refused to write a net label on a gross number until it was corrected.

American Tower’s occupancy N/R was earned, not assumed. The 10-K mentions occupancy five times in prose, so a zero-keyword-hits test would have been wrong. Four candidate windows were reviewed and rejected before the pipeline concluded the metric is not reported, and the windows searched are recorded so the conclusion can be audited.

A reviewer’s rule failed on real data and was replaced. The initial plan flagged any citation spanning three or more numbers as period-risk. On real data that flagged 14 of 14, which prioritises nothing. It was replaced by a count, which ranked the four records already identified as highest-risk at the top.

Key Results

  • 21 cells, every one carrying a status: 14 tied by hand, 6 not reported, 1 flagged rather than guessed. The one NOT TIED cell is Prologis same-store NOI, which the company publishes only as a quarterly series with no full-year figure.
  • 21 of 21 values reproduced byte-identically across two independent extraction runs at temperature 0. The stability check catches drift, not bias; if the model reads the same wrong column twice, the record stays cited, which is what the hand check is for.
  • 99 tests, all offline against a fake model client, covering window finding, every status transition, the N/R-only-after-search rule, hidden-text preservation, the poisoned-value case and the reset-and-re-tie audit path.
  • Every decision that shaped the output, including the two analog calls, the presentation-only unit conversion for lease term, and the history of the verification pass, is dated and recorded in DECISIONS.md.

Comps Table (FY2025)

MetricPLDAMTO
FFO / diluted share (Nareit)N/RN/R$4.25 ✓
Core / Normalized FFO / diluted share5.81 ✓N/R$4.27 ✓
AFFO / diluted shareN/R$10.76 ✓$4.28 ✓
Same-store growth (YoY)⚠ NOT TIED5.1% ✓ (organic tenant billings)1.3% ✓ (rental revenue)
Occupancy (period end)95.8% ✓N/R98.9% ✓
WALT remaining4.2 yrs (50 mo.) ✓N/R8.8 years ✓
Net debt / Adjusted EBITDA5.3x ✓ (gross debt)4.9x ✓5.4x ✓

A check mark means the value was cited verbatim to the filing and confirmed by hand. Every citation, with accession number, document and character offsets, is in the repository’s README so anyone can repeat the check.