How to track insider buying programmatically
You want to know when a CEO or CFO puts their own money into their company's stock. This guide shows you how to pull Form 4 filings filtered to open-market purchases, apply a minimum dollar threshold, and isolate C-suite buys — then wrap it in a Python script you can run on a schedule.
The problem
Form 4 filings are public the moment the SEC accepts them, but the raw EDGAR feed is hard to work with. You'd have to poll the index files, parse XML, map CIKs to tickers, and deduplicate amendments. That pipeline takes a day to build and breaks regularly.
The signal you actually care about is transaction code P — an open-market purchase where the insider used real cash to buy stock at market prices. Everything else (grants, option exercises, gifts) is compensation mechanics or estate planning. Filtering to P with a minimum dollar value cuts noise dramatically.
The approach
The EdgarKit API lets you query Form 4 filings by ticker, transaction code, and minimum trade value in a single request. The workflow is:
- Fetch
P-coded filings for your ticker(s) with amin_valuethreshold - Filter the results down to high-signal roles (CEO, CFO, President)
- Loop on a schedule and print qualifying filings
Step 1: Pull open-market purchases for a single ticker
Start with a bare curl call. The transaction_code=P parameter restricts results to open-market purchases; min_value=50000 drops anything under $50k.
curl "https://api.edgarkit.com/v1/filings?form_type=4&ticker=NVDA&transaction_code=P&min_value=50000&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
The response is a JSON array. Each object includes the filer's name, their role, the transaction date, share count, price, and total value:
{
"filings": [
{
"id": "f4_0001234567_20260615",
"issuer_ticker": "NVDA",
"issuer_name": "NVIDIA Corporation",
"filer_name": "Jensen Huang",
"filer_role": "CEO",
"transaction_date": "2026-06-15",
"transaction_code": "P",
"shares": 5000,
"price_per_share": 118.42,
"total_value": 592100,
"post_transaction_shares": 872500,
"filing_url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=..."
}
],
"next_cursor": "eyJvZmZzZXQiOjIwfQ=="
}
Step 2: Filter to CEO and CFO buys
The filer_role field in each filing lets you filter client-side, or you can pass roles=CEO,CFO as a query parameter to do it server-side:
curl "https://api.edgarkit.com/v1/filings?form_type=4&ticker=TSLA&transaction_code=P&min_value=100000&roles=CEO,CFO&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
The trick is that filer_role is a normalized string EdgarKit extracts from the XML — you don't need to parse the title field yourself. See the Form 4 transaction codes reference for why P at a meaningful dollar size is the one code worth chasing.
Step 3: Query multiple tickers at once
Pass a comma-separated list to ticker to watch a whole basket:
curl "https://api.edgarkit.com/v1/filings?form_type=4&ticker=NVDA,AAPL,TSLA&transaction_code=P&min_value=50000&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
Results are sorted by transaction_date descending. Add since=2026-06-01 to limit to filings from a specific date forward.
Step 4: Paginate through results
The API returns a next_cursor token when there are more results. Pass it as cursor= to get the next page:
curl "https://api.edgarkit.com/v1/filings?form_type=4&ticker=NVDA,AAPL,TSLA&transaction_code=P&min_value=50000&cursor=eyJvZmZzZXQiOjIwfQ==" \
-H "Authorization: Bearer YOUR_API_KEY"
Keep looping until next_cursor is absent or null.
Putting it together
Here's a Python script that runs daily, fetches the last 24 hours of qualifying buys across a watchlist, and prints anything worth reviewing. Drop it in a cron job or a GitHub Actions workflow.
import os
import requests
from datetime import datetime, timedelta, timezone
API_KEY = os.environ["EDGARKIT_API_KEY"]
BASE_URL = "https://api.edgarkit.com/v1/filings"
WATCHLIST = ["NVDA", "AAPL", "TSLA"]
MIN_VALUE = 100_000 # ignore trades under $100k
SIGNAL_ROLES = {"CEO", "CFO", "President", "COO"}
def fetch_insider_buys(tickers: list[str], since: str) -> list[dict]:
params = {
"form_type": "4",
"ticker": ",".join(tickers),
"transaction_code": "P",
"min_value": MIN_VALUE,
"since": since,
"limit": 100,
}
headers = {"Authorization": f"Bearer {API_KEY}"}
results = []
while True:
resp = requests.get(BASE_URL, params=params, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
results.extend(data["filings"])
cursor = data.get("next_cursor")
if not cursor:
break
params["cursor"] = cursor
return results
def main():
since = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%d")
filings = fetch_insider_buys(WATCHLIST, since)
# Surface C-suite buys specifically
signal_filings = [f for f in filings if f.get("filer_role") in SIGNAL_ROLES]
if not signal_filings:
print(f"No qualifying C-suite buys since {since}.")
return
for f in signal_filings:
print(
f"[{f['transaction_date']}] {f['filer_name']} ({f['filer_role']}) "
f"bought {f['shares']:,} shares of {f['issuer_ticker']} "
f"@ ${f['price_per_share']:.2f} = ${f['total_value']:,.0f}"
)
if __name__ == "__main__":
main()
Run it with EDGARKIT_API_KEY=your_key python track_buys.py. Wire it to a cron schedule or a Slack webhook to get daily alerts.
FAQ
What does transaction code P actually mean?
P is an open-market or private purchase — the insider used their own money to buy stock at market prices. It's the strongest bullish signal in the Form 4 universe. Grants, option exercises, and share transfers all use different codes and carry much less signal.
Why filter by min_value?
Sub-$50k buys are often noise. Small trades by directors who have little day-to-day insight into operations rarely move the needle. Setting min_value to $50k or $100k keeps your signal-to-noise ratio reasonable.
How quickly does EdgarKit pick up new filings?
New Form 4 filings typically appear in the API within 30 seconds of SEC acceptance. If you need sub-minute latency, use webhooks instead of polling — see the webhook setup guide for that workflow.
Can I track buys and sales together?
Yes. Omit the transaction_code filter or pass transaction_code=P,S to include both. Sales (S) are noisier because insiders sell for many non-informational reasons. The Form 4 transaction codes reference explains when a sale is actually worth paying attention to.
What if an insider files an amendment (Form 4/A)?
Amendments appear in the feed with form_type: "4/A". If you want to deduplicate against the original, match on filer_cik plus transaction_date plus issuer_cik. EdgarKit flags amended filings with an amended_filing_id field pointing back to the original.