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

EndpointWhat it gives youAccess
/v1/pump-and-dump-trackerRisk scores, IPO details, underwriters, liquidation history, gain/loss dataEnterprise
/v1/ownershipInsider and institutional ownership by reported date, share counts by ownerEnterprise
/v1/historical-float-proFloat, outstanding, and ownership percentages over time from SEC filingsEnterprise
/v1/reverse-splitsReverse split dates, ratios, and execution historyEnterprise
/v1/rofrRight of first refusal and tail financing agreements with banksEnterprise
/v1/agreementsRegistration rights, equity restrictions, participation rights, lockup detailsEnterprise

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 None

The 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 splits

Step 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 offerings
  • registration_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