A common question to put to an AI assistant is "what did this fund buy last quarter?", and the answer comes from Form 13F-HR. I build Equibles, which has an MCP server that answers it. In this post I check one of its answers, Berkshire Hathaway's changes for the quarter ended June 30, 2026, against the filings on SEC EDGAR with a short Python script, and list the traps that make a naive check (or a naive answer) wrong.
1. Ask through MCP
The server is at https://mcp.equibles.com/mcp. In ChatGPT or Claude, add it as a connector; in Claude Code it is one command:
claude mcp add --transport http equibles https://mcp.equibles.com/mcp
Sign-in is OAuth, and the free tier allows 100 requests a day. Setup for other clients is in the MCP docs.
The tool behind a question like "what did Berkshire Hathaway change in its latest 13F?" is GetInstitutionQuarterlyActivity. For this post I called it directly with Berkshire's SEC CIK, 1067983. It compared the June 30, 2026 report with March 31, 2026 and returned these changes:
| Bucket | Ticker | Prior shares | New shares |
|---|---|---|---|
| Initiated | DHI | 0 | 3,564 |
| Increased | GOOGL | 54,249,798 | 78,791,167 |
| Increased | GOOG | 3,585,215 | 27,188,433 |
| Increased | DAL | 39,809,456 | 57,320,000 |
| Increased | LEN | 10,099,642 | 13,111,741 |
| Increased | NYT | 15,146,535 | 15,700,000 |
| Increased | M | 3,038,355 | 7,347,426 |
| Increased | LEN-B | 237,703 | 298,117 |
| Reduced | BAC | 513,624,165 | 483,394,015 |
| Reduced | DVA | 30,100,585 | 28,880,209 |
| Reduced | KR | 50,000,000 | 39,000,000 |
| Reduced | COF | 7,150,000 | 3,000,000 |
| Reduced | NUE | 3,907,075 | 1,857,752 |
| Reduced | ALLY | 29,000,000 | 27,000,000 |
| Exited | STZ | 632,890 | 0 |
Fifteen changes. Now the check.
2. Find the two filings on EDGAR
Every filer's history is in https://data.sec.gov/submissions/CIK0001067983.json. The two 13F-HR filings I need:
- Quarter ended March 31, 2026: accession
0001193125-26-226661, filed May 15, 2026 (index) - Quarter ended June 30, 2026: accession
0001193125-26-352200, filed August 14, 2026 (index)
Each filing folder holds a primary_doc.xml cover page and an information table XML with one <infoTable> element per row.
3. Diff them by CUSIP
The script below uses only the standard library. It finds the information table in each filing folder, sums shares per CUSIP, and prints every position whose share count changed.
"""Diff two 13F-HR filings by CUSIP, straight from SEC EDGAR."""
import json
import sys
import urllib.request
import xml.etree.ElementTree as ET
from collections import defaultdict
# SEC asks automated clients to identify themselves.
HEADERS = {"User-Agent": "your-name your-email@example.com"}
NS = {"t": "http://www.sec.gov/edgar/document/thirteenf/informationtable"}
def get(url):
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req) as resp:
return resp.read()
def holdings(cik, accession):
"""Sum share counts per CUSIP across every row of the information table."""
folder = f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{accession.replace('-', '')}"
files = json.loads(get(f"{folder}/index.json"))["directory"]["item"]
table = next(f["name"] for f in files
if f["name"].endswith(".xml") and f["name"] != "primary_doc.xml")
root = ET.fromstring(get(f"{folder}/{table}"))
shares, names, rows = defaultdict(int), {}, 0
for row in root.findall("t:infoTable", NS):
rows += 1
if row.findtext("t:putCall", default="", namespaces=NS):
continue # options are not share ownership
key = row.findtext("t:cusip", namespaces=NS)
shares[key] += int(row.findtext("t:shrsOrPrnAmt/t:sshPrnamt", namespaces=NS))
names[key] = row.findtext("t:nameOfIssuer", namespaces=NS)
return shares, names, rows
def main(cik, prior_accession, latest_accession):
before, old_names, rows_before = holdings(cik, prior_accession)
after, new_names, rows_after = holdings(cik, latest_accession)
names = {**old_names, **new_names}
print(f"rows: {rows_before} -> {rows_after}, positions: {len(before)} -> {len(after)}")
for key in sorted(set(before) | set(after), key=lambda k: names[k]):
old, new = before.get(key, 0), after.get(key, 0)
if old == new:
continue
label = "NEW" if old == 0 else "EXIT" if new == 0 else "ADD" if new > old else "CUT"
print(f"{label:4} {names[key][:26]:26} {key} {old:>13,} -> {new:>13,}")
if __name__ == "__main__":
main(*sys.argv[1:4])
Put your own name and email in the User-Agent, then run it with the CIK and the two accession numbers:
python3 diff_13f.py 1067983 0001193125-26-226661 0001193125-26-352200
Output from my run on October 8, 2026:
rows: 90 -> 89, positions: 29 -> 29
CUT ALLY FINL INC 02005N100 29,000,000 -> 27,000,000
ADD ALPHABET INC 02079K305 54,249,798 -> 78,791,167
ADD ALPHABET INC 02079K107 3,585,215 -> 27,188,433
CUT BANK OF AMER CORP 060505104 513,624,165 -> 483,394,015
CUT CAPITAL ONE FINL CORP 14040H105 7,150,000 -> 3,000,000
EXIT CONSTELLATION BRANDS INC 21036P108 632,890 -> 0
NEW D R HORTON INC 23331A109 0 -> 3,564
CUT DAVITA INC 23918K108 30,100,585 -> 28,880,209
ADD DELTA AIR LINES INC 247361702 39,809,456 -> 57,320,000
CUT KROGER CO 501044101 50,000,000 -> 39,000,000
ADD LENNAR CORP 526057104 10,099,642 -> 13,111,741
ADD LENNAR CORP 526057302 237,703 -> 298,117
ADD MACYS INC 55616P104 3,038,355 -> 7,347,426
ADD NEW YORK TIMES CO MTN BE 650111107 15,146,535 -> 15,700,000
CUT NUCOR CORP 670346105 3,907,075 -> 1,857,752
All fifteen changes match the MCP table share for share, including the 3,564-share D.R. Horton position and both Alphabet classes.
4. Five traps the script handles (and an answer should too)
One position is many rows. The June filing has 89 rows but only 29 distinct CUSIPs. Berkshire splits a position across rows by the combination of reporting managers listed for it, and 19 of the 29 positions span more than one row; Bank of America alone takes 8. Compare single rows across quarters and you will report trades that never happened. Sum per CUSIP first.
Key on CUSIP, never on the issuer name. Issuer names are free text typed by the filer, and they drift. In the March filing the bank is BANK AMERICA CORP and the insurer is CHUBB LTD SWITZ; in June they are BANK OF AMER CORP and CHUBB LIMITED. A diff keyed on names reports that Berkshire sold its entire Bank of America stake and bought 483 million shares of a "new" company, then does the same for Chubb.
A change in value is not a trade. Each row carries a reported dollar value. Berkshire's Bank of America share count fell by 30,230,150, yet the reported value rose from $25.04 billion to $27.54 billion because the price went up. New York Times went the other way: 553,465 more shares, $169.5 million less value. Rank by share change when the question is what the fund did.
Share classes are separate lines. Alphabet Class A (02079K305) and Class C (02079K107) have different CUSIPs. Berkshire added to both, 24.5 million and 23.6 million shares, so "Berkshire added 24.5 million Alphabet shares" leaves out half the buying.
Size before headline. "Berkshire opened a position in D.R. Horton" is accurate and nearly meaningless: 3,564 shares with a reported value of $580,504, about 0.0002% of the $299.25 billion the filing reports in total. A good answer puts the size next to the label.
One timing caveat applies to all of it: a 13F shows holdings on the last day of the quarter and can be filed up to 45 days later, so this filing describes June 30, 2026, not today. It also covers only long positions in 13(f) securities. Options appear as rows with a putCall field, which the script skips (the June filing has none).
5. Make it routine
When an assistant answers a 13F question, ask it for the report date and the accession number, then run a check like this whenever the answer matters. On the Equibles side, the tool reference is in the holdings docs, the same data is available through the REST API for scheduled jobs, and Berkshire's filings are browsable on its institution page.
Disclosure: I build Equibles. This post was written by an AI agent working from the EDGAR filings named above; every figure was re-derived from them on October 8, 2026, and the script output is from an actual run. Nothing here is investment advice.
Top comments (0)