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 two Enterprise-only endpoints — ROFR and agreements — which aren't available on Retail plans. The rest (pump-and-dump tracker, ownership, historical float, reverse splits) are available on paid 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 dataPaid plans
/v1/ownershipInsider and institutional ownership by reported date, share counts by ownerPaid plans
/v1/historical-float-proFloat, outstanding, and ownership percentages over time from SEC filingsPaid plans
/v1/reverse-splitsReverse split dates, ratios, and execution historyPaid plans
/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://eapi.askedgar.io"
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()
    return resp.json()["results"]

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


Did this page help you?