Due Diligence Report
Build a one-click due diligence page that surfaces red flags in IPOs and low-float stocks before you take a position.
This example uses Enterprise-only endpoints including pump-and-dump tracker, ownership, historical float, ROFR, and agreements. These are not available on Retail plans. Contact us to discuss Enterprise access.
You find a low-float stock running 40% premarket. It IPO'd three months ago, has a Chinese shell structure, and the underwriter is a name you've never heard of. Is this a real opportunity or a setup?
That's the question this due diligence report answers in seconds. It pulls structured data from six endpoints and assembles a single-page report covering ownership, float history, capital structure, insider agreements, and pump-and-dump risk signals β everything you'd normally spend an hour digging through EDGAR to find.
What you're building
A report generator that takes a ticker and produces:
- A risk summary with country, underwriter, and scam risk scores
- Ownership breakdown β who holds what, and how concentrated it is
- Float history β how the float has changed over time (dilution in action)
- Reverse split history β how many times the company has consolidated shares
- ROFR and tail financing agreements β which banks have ongoing rights to future deals
- Registration rights and equity restrictions β lockup expirations and participation rights
The data sources
| Endpoint | What it gives you | Access |
|---|---|---|
/v1/pump-and-dump-tracker | Risk scores, IPO details, underwriters, liquidation history, gain/loss data | Enterprise |
/v1/ownership | Insider and institutional ownership by reported date, share counts by owner | Enterprise |
/v1/historical-float-pro | Float, outstanding, and ownership percentages over time from SEC filings | Enterprise |
/v1/reverse-splits | Reverse split dates, ratios, and execution history | Enterprise |
/v1/rofr | Right of first refusal and tail financing agreements with banks | Enterprise |
/v1/agreements | Registration rights, equity restrictions, participation rights, lockup details | Enterprise |
Step 1: Pull the pump-and-dump tracker
This is the fastest way to get a risk overview. The tracker scores stocks across multiple dimensions and flags known patterns.
import requests
from datetime import date
API_KEY = "your_api_key"
BASE_URL = "https://api.askedgar.com"
HEADERS = {"API-KEY": API_KEY}
def get_pnd_data(ticker):
resp = requests.get(
f"{BASE_URL}/v1/pump-and-dump-tracker",
params={"ticker": ticker},
headers=HEADERS,
)
resp.raise_for_status()
results = resp.json()["results"]
return results[0] if results else NoneThe response packs a lot into a single object:
{
"ticker": "ABCD",
"ipo_date": "2023-01-15",
"lock_up_expiration": "2023-07-15",
"underwriters": "Example Underwriter LLC",
"country": "CN",
"country_risk": "high",
"underwriter_risk": "high",
"float_risk": "high",
"scam_risk": "high",
"scam_description": "Suspected pump and dump scheme",
"number_liquidations": 3,
"last_liquidation_date": "2023-09-01",
"tradable_float": 2500000,
"outstanding_shares": 10000000,
"market_cap": 15000000,
"is_asian": true,
"isadr": false,
"known_underwriter": true,
"known_ipo_underwriter": true,
"num_splits": 2,
"last_split_date": "2023-06-01",
"gain_1_day": -15.5,
"gain_7_day": -30.25,
"gain_14_day": -45.0,
"gain_30_day": -60.75
}The risk fields (country_risk, underwriter_risk, float_risk, scam_risk) are each rated high, medium, or low. Multiple highs in a single stock is a strong red flag.
Step 2: Pull ownership structure
Who actually owns this company? Concentrated insider ownership, especially combined with upcoming lockup expirations, tells you where selling pressure might come from.
def get_ownership(ticker):
resp = requests.get(
f"{BASE_URL}/v1/ownership",
params={"ticker": ticker, "limit": 50},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()["results"]
def summarize_ownership(ownership_groups):
if not ownership_groups:
return None
# Use the most recent reported date
latest = ownership_groups[0]
owners = latest.get("owners", [])
total_common = sum(o.get("common_shares_amount", 0) or 0 for o in owners)
insiders = [o for o in owners if o.get("owner_type") in ("director", "officer", "insider")]
institutions = [o for o in owners if o.get("owner_type") == "institution"]
return {
"reported_date": latest.get("reported_date"),
"total_owners": len(owners),
"insider_count": len(insiders),
"institution_count": len(institutions),
"total_common_shares": total_common,
"top_holders": [
{"name": o.get("owner_name"), "shares": o.get("common_shares_amount", 0), "type": o.get("owner_type")}
for o in sorted(owners, key=lambda x: x.get("common_shares_amount", 0) or 0, reverse=True)[:5]
],
}Step 3: Track float changes over time
A float that's been steadily increasing tells you dilution has already been happening. Compare the earliest and most recent filings to see the trend.
def get_float_history(ticker):
resp = requests.get(
f"{BASE_URL}/v1/historical-float-pro",
params={"ticker": ticker, "limit": 50},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()["results"]
def analyze_float_trend(history):
if len(history) < 2:
return None
# History is ordered by filing date β compare earliest to latest
earliest = history[-1]
latest = history[0]
os_change = (latest["outstanding_shares"] - earliest["outstanding_shares"])
os_change_pct = (os_change / earliest["outstanding_shares"] * 100) if earliest["outstanding_shares"] else None
float_change = (latest["float"] - earliest["float"])
float_change_pct = (float_change / earliest["float"] * 100) if earliest["float"] else None
return {
"period": f"{earliest['reported_date']} β {latest['reported_date']}",
"filings_count": len(history),
"outstanding_shares_earliest": earliest["outstanding_shares"],
"outstanding_shares_latest": latest["outstanding_shares"],
"outstanding_change_pct": round(os_change_pct, 1) if os_change_pct else None,
"float_earliest": earliest["float"],
"float_latest": latest["float"],
"float_change_pct": round(float_change_pct, 1) if float_change_pct else None,
"insider_pct_latest": latest.get("insider_percent"),
"institutional_pct_latest": latest.get("institutions_percent"),
"is_foreign": latest.get("foreign_company"),
"is_adr": latest.get("is_adr"),
}Step 4: Check reverse split history
Multiple reverse splits are one of the strongest signals that a company is a chronic diluter β they issue shares, the price drops, they reverse split to regain compliance, then repeat.
def get_reverse_splits(ticker):
resp = requests.get(
f"{BASE_URL}/v1/reverse-splits",
params={"ticker": ticker, "limit": 50},
headers=HEADERS,
)
resp.raise_for_status()
data = resp.json()["results"]
# The response wraps splits in a nested structure
splits = []
for group in data:
for s in group.get("results", []):
splits.append(s)
return splitsStep 5: Pull bank agreements and ROFR
Right of first refusal agreements lock a company into working with a specific bank on future deals. Tail financing provisions mean the bank earns fees even on deals they didn't arrange. These are forward-looking signals of where the next offering will come from.
def get_rofr(ticker):
resp = requests.get(
f"{BASE_URL}/v1/rofr",
params={"ticker": ticker, "limit": 50},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()["results"]
def get_agreements(ticker):
resp = requests.get(
f"{BASE_URL}/v1/agreements",
params={"ticker": ticker, "limit": 50},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()["results"]Key things to surface from agreements:
equity_restriction_end_dateβ when lockup restrictions expire (potential selling pressure)is_right_of_participationβ whether investors have the right to participate in future offeringsregistration_deadline_dateβ when the company must register shares for resale
Putting it together
A complete report builder that pulls all six data sources into a single output:
def build_dd_report(ticker):
pnd = get_pnd_data(ticker)
ownership = get_ownership(ticker)
float_history = get_float_history(ticker)
splits = get_reverse_splits(ticker)
rofr = get_rofr(ticker)
agreements = get_agreements(ticker)
# Risk summary
risk_summary = None
if pnd:
risk_fields = ["country_risk", "underwriter_risk", "float_risk", "scam_risk"]
high_count = sum(1 for f in risk_fields if pnd.get(f) == "high")
risk_summary = {
"overall_flags": high_count,
"country": pnd.get("country"),
"country_risk": pnd.get("country_risk"),
"underwriter": pnd.get("underwriters"),
"underwriter_risk": pnd.get("underwriter_risk"),
"float_risk": pnd.get("float_risk"),
"scam_risk": pnd.get("scam_risk"),
"scam_description": pnd.get("scam_description"),
"ipo_date": pnd.get("ipo_date"),
"lockup_expiration": pnd.get("lock_up_expiration"),
"liquidation_count": pnd.get("number_liquidations"),
"last_liquidation": pnd.get("last_liquidation_date"),
"num_reverse_splits": pnd.get("num_splits"),
}
# Float trend
float_trend = analyze_float_trend(float_history) if float_history else None
# Ownership summary
ownership_summary = summarize_ownership(ownership) if ownership else None
# Upcoming lockup expirations from agreements
upcoming_lockups = [
{
"investor": a.get("investor_names"),
"end_date": a.get("equity_restriction_end_date"),
"type": a.get("agreement_type"),
}
for a in agreements
if a.get("equity_restriction_end_date")
and a["equity_restriction_end_date"] >= date.today().isoformat()
]
# Active ROFR agreements
active_rofr = [
{
"bank": r.get("bank_name"),
"type": r.get("right_of_first_refusal_type"),
"end_date": r.get("right_of_first_refusal_end_date"),
"has_tail": r.get("tail_financing_payments_present"),
"tail_duration_months": r.get("tail_financing_payments_duration"),
}
for r in rofr
if r.get("right_of_first_refusal_present")
]
return {
"ticker": ticker,
"risk_summary": risk_summary,
"ownership": ownership_summary,
"float_trend": float_trend,
"reverse_splits": [
{"date": s["execution_date"], "ratio": f"{s['split_from']}:{s['split_to']}"}
for s in splits
],
"active_rofr": active_rofr,
"upcoming_lockups": upcoming_lockups,
"total_agreements": len(agreements),
}Printing the report
def print_report(report):
t = report["ticker"]
r = report.get("risk_summary")
f = report.get("float_trend")
o = report.get("ownership")
print(f"\n{'='*60}")
print(f" DUE DILIGENCE REPORT: {t}")
print(f"{'='*60}")
if r:
print(f"\nπ RISK SUMMARY ({r['overall_flags']}/4 high-risk flags)")
print(f" Country: {r['country']} ({r['country_risk']} risk)")
print(f" Underwriter: {r['underwriter']} ({r['underwriter_risk']} risk)")
print(f" Float Risk: {r['float_risk']}")
print(f" Scam Risk: {r['scam_risk']}")
if r.get("scam_description"):
print(f" β οΈ {r['scam_description']}")
print(f" IPO: {r['ipo_date']} | Lockup expires: {r['lockup_expiration']}")
print(f" Liquidations: {r['liquidation_count']} | Reverse splits: {r['num_reverse_splits']}")
if f:
print(f"\nπ FLOAT TREND ({f['period']})")
print(f" Outstanding: {f['outstanding_shares_earliest']:,} β {f['outstanding_shares_latest']:,} ({f['outstanding_change_pct']:+.1f}%)")
print(f" Float: {f['float_earliest']:,} β {f['float_latest']:,} ({f['float_change_pct']:+.1f}%)")
print(f" Insider: {f.get('insider_pct_latest', '?')}% | Institutional: {f.get('institutional_pct_latest', '?')}%")
if o:
print(f"\nπ₯ OWNERSHIP (as of {o['reported_date']})")
print(f" {o['insider_count']} insiders, {o['institution_count']} institutions")
for h in o["top_holders"]:
print(f" β’ {h['name']}: {h['shares']:,} shares ({h['type']})")
splits = report.get("reverse_splits", [])
if splits:
print(f"\nπ REVERSE SPLITS ({len(splits)} total)")
for s in splits:
print(f" β’ {s['date']}: {s['ratio']}")
rofr = report.get("active_rofr", [])
if rofr:
print(f"\nπ¦ ACTIVE ROFR AGREEMENTS ({len(rofr)})")
for r in rofr:
tail = f" + tail ({r['tail_duration_months']}mo)" if r.get("has_tail") else ""
print(f" β’ {r['bank']}: {r['type']} until {r['end_date']}{tail}")
lockups = report.get("upcoming_lockups", [])
if lockups:
print(f"\nπ UPCOMING LOCKUP EXPIRATIONS ({len(lockups)})")
for l in lockups:
print(f" β’ {l['investor']}: {l['end_date']}")
print(f"\n{'='*60}\n")Running it
report = build_dd_report("ABCD")
print_report(report)Example output:
============================================================
DUE DILIGENCE REPORT: ABCD
============================================================
π RISK SUMMARY (4/4 high-risk flags)
Country: CN (high risk)
Underwriter: Example Underwriter LLC (high risk)
Float Risk: high
Scam Risk: high
β οΈ Suspected pump and dump scheme
IPO: 2023-01-15 | Lockup expires: 2023-07-15
Liquidations: 3 | Reverse splits: 2
π FLOAT TREND (2023-03-15 β 2024-09-30)
Outstanding: 10,000,000 β 45,000,000 (+350.0%)
Float: 2,500,000 β 18,000,000 (+620.0%)
Insider: 12.5% | Institutional: 3.2%
π₯ OWNERSHIP (as of 2024-09-30)
3 insiders, 1 institutions
β’ John Doe: 4,500,000 shares (director)
β’ Jane Smith: 2,200,000 shares (officer)
β’ Entity Holdings Ltd: 1,800,000 shares (institution)
β’ Bob Chen: 500,000 shares (insider)
β’ Alice Wang: 200,000 shares (director)
π REVERSE SPLITS (2 total)
β’ 2023-06-01: 2:1
β’ 2024-03-15: 10:1
π¦ ACTIVE ROFR AGREEMENTS (1)
β’ Example Capital: exclusive until 2025-06-01 + tail (12mo)
π UPCOMING LOCKUP EXPIRATIONS (2)
β’ Entity Holdings Ltd: 2025-03-15
β’ Offshore Fund LP: 2025-06-30
============================================================
This stock has all four risk categories flagged high, a 620% float increase, two reverse splits, and an upcoming lockup expiration. That's the kind of report that takes 5 seconds to generate and saves you from a trade you'd regret.
Red flags cheat sheet
Some patterns to watch for when reading the report:
- 4/4 high risk flags β every dimension is flagged, this is a textbook setup to avoid
- Float increased 200%+ while outstanding shares tripled β aggressive dilution already happened
- Multiple reverse splits β the dilute-split-dilute cycle, one of the strongest negative signals
- ROFR with tail financing β even if the company switches banks, the old bank still gets paid, which means the relationship is sticky and more deals are likely
- Lockup expirations within 30 days β insider selling pressure may be imminent
- Foreign company with unknown underwriter β higher fraud risk, less regulatory oversight
Cost considerations
This report makes 6 API calls per ticker. For one-off due diligence that's negligible. If you're screening a batch of tickers, pull the pump-and-dump tracker data first (it includes risk scores and basic float data) and only generate the full report for tickers that warrant deeper investigation.
Next steps
Updated about 3 hours ago