Turkcell, “5G” reklamları için Shaquille O’Neal ile anlaştı

Turkcell

TurkcellTurkcell, resmi olarak 1 Nisan tarihinde devreye alınacak 5G hizmetinin reklamları için efsane basketbolcu Shaquille O’Neal ile anlaştığını duyurdu. Bu konuda yapılan açıklamada, “Turkcell’in, 5G iletişimi kapsamındaki reklam yüzü basketbol tarihinin en ikonik isimlerinden Shaquille O’Neal oldu. Basketbol efsanesi, 1 Nisan itibarıyla Türkiye’de kullanılmaya başlanacak olan Turkcell gücünde 5G’nin iletişimi için ocak ayında Türkiye’ye gelmişti. Yayınlanan teaser filminde Shaq’ın İstanbul Havalimanı’nda yürümesi, onu görenlerin şaşkınlığı ve Shaq ile selamlaşması görülüyor. Teaser filmi, ekrana gelen ‘Turkcell Gücünde 5G GELİYOR’ yazısıyla sona eriyor. Turkcell’in 5G dönemine taşıdığı gücün, hızın ve özgüvenin yansıması olacak reklam filminin dün yayına giren teaser’ı büyük heyecan uyandırdı. Teaser’ın Turkcell’in sosyal medya adreslerinden “Turkcell gücünde 5G, ‘dev’ sürprizleriyle yakında” ifadeleriyle post edilmesi, kampanyanın detaylarına da bir gönderme yapıyor. Kısa tanıtımı izleyen herkes yayına girecek olan reklam filminin uzun kopyasını merakla beklemeye başladı.” ifadeleri kullanıldı.

Turkcell, “5G” reklamları için Shaquille O’Neal ile anlaştı yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Scraping YellowPages with Python in 2026: What Actually Works (and What Doesn’t)

I spent a week trying to scrape YellowPages.com with Python. Most tutorials online are from 2021-2023 and silently fail now. Here’s what I found.

The tutorials are all broken

If you Google “scrape yellowpages python”, you’ll find guides using requests + BeautifulSoup. They look clean, they make sense, and they don’t work.

import requests
from bs4 import BeautifulSoup

url = "https://www.yellowpages.com/search?search_terms=plumbers&geo_location_terms=Austin%2C+TX"
resp = requests.get(url)
# resp.status_code == 403. Every time.

YellowPages.com moved behind Cloudflare sometime in 2023. Every request now passes through a JavaScript challenge. requests can’t execute JavaScript, so it gets a 403 or an empty challenge page. Same with httpx, urllib3, or any pure HTTP library.

What about Selenium/Playwright?

Headless browsers can execute the JS challenge:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://www.yellowpages.com/search?search_terms=plumbers&geo_location_terms=Austin%2C+TX")
    page.wait_for_selector(".search-results")
    html = page.content()

This works… sometimes. Three problems kill it at scale:

1. IP blocks. Default Chromium uses your IP. After 10-20 requests, Cloudflare blocks you. You need residential proxies, and specifically US ones — non-US IPs get rejected.

2. Browser fingerprinting. Headless Chrome has detectable fingerprint differences. Cloudflare catches navigator.webdriver=true, missing plugins, and other signals. You need stealth patches.

3. Session reuse kills extraction. If you navigate to 50 business detail pages in one browser session, Cloudflare starts returning challenge pages instead of data after the 8th or 9th page. You need fresh browser contexts per page.

The proxy problem

Even with Playwright + stealth, you need proxies. Not just any proxies — residential US proxies specifically.

Free proxy lists? Dead within hours. Datacenter proxies? Blocked instantly. Shared residential proxies? Rate-limited because 100 other scrapers are on the same pool.

You’ll spend $50-100/month on a residential proxy service just to keep the scraper running. Then you need rotation logic, error handling for burned IPs, and retry logic.

What I actually use now

After fighting this for a week, I switched to using a pre-built scraper that handles all of this: Yellow Pages Scraper on Apify.

It handles:

  • Cloudflare bypass with residential proxies (built in)
  • Stealth browser automation (no fingerprint detection)
  • Fresh browser sessions per detail page
  • Automatic retries on blocked requests
  • Structured output (JSON, CSV, Excel)

You put in search terms + locations and get back clean data:

{
  "name": "Joe's Plumbing LLC",
  "phone": "(512) 555-0142",
  "email": "[email protected]",
  "website": "https://joesplumbing.com",
  "address": "1234 Main St, Austin, TX 78701",
  "rating": 4.5,
  "reviewCount": 47,
  "categories": ["Plumbers", "Water Heaters"],
  "yearsInBusiness": 12
}

Cost: about $0.005 per result. A run of 100 businesses costs $0.60. Cheaper than maintaining your own proxy subscription.

When to roll your own

Building your own scraper makes sense if:

  • You need custom post-processing that can’t be done after export
  • You’re scraping at massive scale (10K+ results/day) and want to optimize costs
  • You need real-time streaming rather than batch results
  • You want to learn how browser automation and anti-bot bypass work

If you just need business leads from YellowPages, the pre-built tool saves you a week of proxy debugging.

Key technical lessons if you do build it yourself

  1. Use Go or Playwright, not Selenium. Selenium is slower and has more detectable fingerprints.
  2. Residential US proxies are mandatory. Budget $50-100/month minimum.
  3. Fresh browser context per detail page. Session reuse drops email extraction from ~22% to ~3%.
  4. Parse JSON-LD, not HTML. YellowPages embeds structured data as application/ld+json. It’s cleaner and more stable than CSS selectors.
  5. Respect rate limits. 1-3 seconds between requests. Faster than that triggers immediate blocks.

I’m a data engineer building automation tools for lead generation. If you have questions about scraping public business directories, drop a comment.

Make Your Queries 10x Faster with GBase 8a Rough Index

When working with big data analytics in GBase 8a, you’ve probably noticed something strange: the same SQL query can be incredibly fast sometimes, yet painfully slow at others. The key difference often comes down to whether you’re leveraging Rough Index — a lightweight indexing mechanism built into the GBase 8a columnar storage engine.

In this article, I’ll walk you through what Rough Index is, how it works, and how to use it effectively to dramatically speed up your queries.

  1. What Is Rough Index?
    Rough Index (RI) is a block‑level index that doesn’t store exact row positions like a B‑Tree. Instead, for each data block (typically 64KB or 128KB), it records the minimum and maximum values of a column within that block.

Example: Suppose the amount column in an orders table is split into 1,000 blocks. Rough Index might store:

Block 1: min=10, max=500
Block 2: min=501, max=1200
Block 3: min=1201, max=2500
…

Now when you run a query like WHERE amount > 2000, the GBase 8a optimizer scans the Rough Index first. Any block whose maximum value is ≤ 2000 is skipped entirely — no actual data is read. This is called block pruning.

When data is well‑ordered, Rough Index can prune 90% or more of I/O operations, making your queries 5 to 10 times faster with no extra hardware.

  1. How to Check If Rough Index Is Working
    Use EXPLAIN to View the Execution Plan
EXPLAIN SELECT SUM(amount) FROM orders WHERE create_time >= '2024-01-01';

If you see “Rough Index Scan” in the output, or the number of blocks_scanned is much smaller than the total block count, Rough Index is active.

Query the System View

SELECT * FROM information_schema.GBASE_ROUGH_INDEX 
WHERE table_name = 'orders';

This view shows which columns have Rough Index enabled and their current status.

  1. How to Maximize Rough Index Effectiveness
    Tip 1: Keep Data Ordered During Import
    Rough Index pruning works best when data is sorted by the column you frequently filter on. If values are randomly distributed across blocks, each block’s min/max range becomes wide, and pruning becomes less effective.
    Best practice: Sort your input file by the filter column before loading.
LOAD DATA INFILE '/data/orders_sorted.csv' INTO TABLE orders
FIELDS TERMINATED BY ',' (order_id, user_id, amount, create_time);
-- Make sure the file is sorted by create_time before loading

Tip 2: Rebuild Rough Index After Updates
If your table undergoes many updates or out‑of‑order inserts, you can manually rebuild the index:

ALTER TABLE orders REBUILD ROUGH INDEX;

Tip 3: Use Range Filters for Best Results
Rough Index performs best with range predicates (>, <, BETWEEN). It works less well with IN lists or !=.
✅ Good: Range query

SELECT * FROM orders 
WHERE create_time BETWEEN '2024-01-01' AND '2024-03-31';

❌ Not ideal: Discrete value list

SELECT * FROM orders 
WHERE status IN (1, 3, 7, 15);
  1. Summary
    Rough Index is a powerful, built‑in performance feature in the gbase database. By understanding its block‑pruning mechanism and designing your data loading and queries accordingly, you can achieve massive speedups without adding hardware.

If you’re using GBase 8a for analytical workloads, take a moment to check your Rough Index usage — it might be the secret to making your queries run 10x faster!

Self-Supervised Temporal Pattern Mining for satellite anomaly response operations for extreme data sparsity scenarios

Self-Supervised Temporal Pattern Mining for Satellite Anomaly Response

Self-Supervised Temporal Pattern Mining for satellite anomaly response operations for extreme data sparsity scenarios

Introduction: The Silent Satellite Problem

I remember the first time I encountered what we called “the silent satellite problem” during my research at the European Space Agency’s data lab. We were analyzing telemetry from a decade-old Earth observation satellite that had started exhibiting mysterious power fluctuations. The anomaly logs were sparse—sometimes weeks between meaningful events—and the labeled data was practically non-existent. Traditional supervised approaches failed spectacularly, with our best models achieving barely 40% precision on anomaly detection.

This experience fundamentally changed my approach to AI for space systems. While exploring alternative methodologies, I discovered that the most valuable patterns weren’t in the anomalies themselves, but in the temporal relationships between seemingly normal operations. Through studying recent advances in self-supervised learning, I realized we could treat the satellite’s entire operational history as a single, continuous learning signal, even with extreme data sparsity.

Technical Background: The Challenge of Sparse Space Data

Satellite anomaly response presents unique challenges that make conventional machine learning approaches inadequate:

  1. Extreme Data Sparsity: Anomalies might occur only once every few months
  2. High-Dimensional Temporal Data: Hundreds of telemetry channels with complex interdependencies
  3. Missing Labels: Most anomalies are discovered post-facto, if at all
  4. Non-Stationary Systems: Satellites age, components degrade, and operational patterns evolve

During my investigation of self-supervised approaches for time series, I found that contrastive learning methods could learn robust representations even with sparse positive examples. The key insight was that temporal consistency—how systems behave over time—provides a powerful supervisory signal without explicit labels.

Core Methodology: Temporal Contrastive Learning

The Temporal Consistency Principle

One interesting finding from my experimentation with satellite telemetry was that normal operations exhibit predictable temporal patterns, while anomalies break these patterns in subtle ways. We can leverage this through temporal contrastive learning:

import torch
import torch.nn as nn
import torch.nn.functional as F

class TemporalContrastiveEncoder(nn.Module):
    def __init__(self, input_dim=128, hidden_dim=256, output_dim=128):
        super().__init__()
        self.temporal_encoder = nn.LSTM(
            input_dim, hidden_dim,
            num_layers=3,
            bidirectional=True,
            dropout=0.2,
            batch_first=True
        )
        self.projection_head = nn.Sequential(
            nn.Linear(hidden_dim * 2, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim)
        )

    def forward(self, x, lengths):
        packed = nn.utils.rnn.pack_padded_sequence(
            x, lengths.cpu(),
            batch_first=True,
            enforce_sorted=False
        )
        outputs, (hidden, cell) = self.temporal_encoder(packed)
        hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)
        return F.normalize(self.projection_head(hidden), dim=1)

class TemporalContrastiveLoss(nn.Module):
    def __init__(self, temperature=0.1):
        super().__init__()
        self.temperature = temperature

    def forward(self, z_i, z_j):
        """Normalized temperature-scaled cross entropy loss"""
        batch_size = z_i.size(0)
        labels = torch.arange(batch_size).to(z_i.device)

        # Compute similarity matrix
        features = torch.cat([z_i, z_j], dim=0)
        similarity_matrix = F.cosine_similarity(
            features.unsqueeze(1),
            features.unsqueeze(0),
            dim=2
        )

        # Mask for positive pairs
        mask = torch.eye(batch_size, dtype=torch.bool).to(z_i.device)
        mask = mask.repeat(2, 2)
        mask.fill_diagonal_(0)

        positives = similarity_matrix[mask].view(2 * batch_size, -1)
        negatives = similarity_matrix[~mask].view(2 * batch_size, -1)

        logits = torch.cat([positives, negatives], dim=1)
        logits /= self.temperature

        loss = F.cross_entropy(logits, labels.repeat(2))
        return loss

Through studying this approach, I learned that creating positive pairs through temporal augmentation (shifting, scaling, and masking time windows) allows the model to learn invariant representations of normal operational patterns.

Multi-Scale Temporal Pattern Mining

My exploration of satellite telemetry revealed that anomalies manifest at different temporal scales. Some appear as sudden spikes, while others develop gradually over weeks. This led me to develop a multi-scale architecture:

class MultiScaleTemporalMiner(nn.Module):
    def __init__(self, input_channels, scales=[1, 7, 30]):
        super().__init__()
        self.scales = scales
        self.encoders = nn.ModuleList([
            TemporalEncoder(input_channels, scale_length=s)
            for s in scales
        ])
        self.attention = nn.MultiheadAttention(
            embed_dim=256,
            num_heads=8,
            dropout=0.1,
            batch_first=True
        )
        self.anomaly_scorer = nn.Sequential(
            nn.Linear(256 * len(scales), 512),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Linear(256, 1)
        )

    def forward(self, x):
        """Process multi-scale temporal patterns"""
        scale_features = []

        for scale, encoder in zip(self.scales, self.encoders):
            # Extract features at each temporal scale
            scaled_x = self._resample_temporal(x, scale)
            features = encoder(scaled_x)
            scale_features.append(features)

        # Cross-scale attention
        combined = torch.cat(scale_features, dim=1)
        attended, _ = self.attention(combined, combined, combined)

        # Anomaly scoring
        scores = self.anomaly_scorer(attended)
        return scores, attended

    def _resample_temporal(self, x, scale_factor):
        """Adaptive temporal resampling"""
        if scale_factor == 1:
            return x

        batch_size, seq_len, features = x.shape
        new_len = seq_len // scale_factor

        # Use adaptive pooling for temporal resampling
        x_reshaped = x.transpose(1, 2).contiguous()
        resampled = F.adaptive_avg_pool1d(x_reshaped, new_len)
        return resampled.transpose(1, 2).contiguous()

During my experimentation with this architecture, I observed that the attention mechanism learns to weight different temporal scales based on the anomaly type, providing interpretable insights into failure modes.

Implementation Details: Handling Extreme Sparsity

Synthetic Minority Oversampling for Time Series

One of the biggest challenges I encountered was the extreme class imbalance. While researching solutions, I developed a temporal-aware oversampling technique:

import numpy as np
from scipy.interpolate import CubicSpline

class TemporalSMOTE:
    def __init__(self, k_neighbors=5, synthetic_ratio=0.5):
        self.k = k_neighbors
        self.ratio = synthetic_ratio

    def generate_synthetic(self, minority_samples, timestamps):
        """Generate synthetic temporal sequences"""
        n_samples, seq_len, n_features = minority_samples.shape
        n_synthetic = int(n_samples * self.ratio)

        synthetic_samples = []
        synthetic_timestamps = []

        for _ in range(n_synthetic):
            # Randomly select two samples
            idx1, idx2 = np.random.choice(n_samples, 2, replace=False)

            # Temporal alignment using DTW distance
            aligned_sample = self._temporal_interpolation(
                minority_samples[idx1],
                minority_samples[idx2],
                timestamps[idx1],
                timestamps[idx2]
            )

            synthetic_samples.append(aligned_sample)

            # Generate synthetic timestamp
            synthetic_ts = self._interpolate_timestamp(
                timestamps[idx1],
                timestamps[idx2]
            )
            synthetic_timestamps.append(synthetic_ts)

        return np.array(synthetic_samples), np.array(synthetic_timestamps)

    def _temporal_interpolation(self, seq1, seq2, ts1, ts2):
        """Dynamic time warping interpolation"""
        # Simplified DTW-based interpolation
        n_features = seq1.shape[1]
        interpolated = np.zeros_like(seq1)

        for f in range(n_features):
            # Create cubic splines for both sequences
            cs1 = CubicSpline(ts1, seq1[:, f])
            cs2 = CubicSpline(ts2, seq2[:, f])

            # Interpolate at combined time points
            combined_ts = np.sort(np.unique(np.concatenate([ts1, ts2])))
            interp1 = cs1(combined_ts)
            interp2 = cs2(combined_ts)

            # Weighted combination
            alpha = np.random.random()
            interpolated[:, f] = alpha * interp1 + (1 - alpha) * interp2

        return interpolated

Through studying various oversampling techniques, I found that temporal-aware methods significantly outperformed traditional SMOTE for time series anomaly detection.

Uncertainty-Aware Anomaly Scoring

My research revealed that quantifying uncertainty is crucial for operational decision-making. I implemented a Bayesian approach:

import pyro
import pyro.distributions as dist
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam

class BayesianAnomalyDetector(nn.Module):
    def __init__(self, input_dim, hidden_dim=128):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim // 2)
        )

    def model(self, x, y=None):
        # Priors
        w_prior = dist.Normal(torch.zeros_like(self.encoder[0].weight), 1.)
        b_prior = dist.Normal(torch.zeros_like(self.encoder[0].bias), 1.)

        priors = {'encoder.0.weight': w_prior, 'encoder.0.bias': b_prior}

        # Lift parameters
        lifted_module = pyro.random_module("module", self.encoder, priors)
        lifted_reg_model = lifted_module()

        with pyro.plate("data", x.shape[0]):
            # Forward pass
            hidden = lifted_reg_model(x)
            mean = hidden.mean(1, keepdim=True)

            # Observation model
            obs = dist.Normal(mean, 0.1)
            pyro.sample("obs", obs, obs=y)

    def guide(self, x, y=None):
        # Variational parameters
        w_mu = pyro.param("w_mu", torch.randn_like(self.encoder[0].weight))
        w_sigma = pyro.param("w_sigma", torch.ones_like(self.encoder[0].weight),
                           constraint=dist.constraints.positive)

        b_mu = pyro.param("b_mu", torch.randn_like(self.encoder[0].bias))
        b_sigma = pyro.param("b_sigma", torch.ones_like(self.encoder[0].bias),
                           constraint=dist.constraints.positive)

        # Variational distributions
        w_dist = dist.Normal(w_mu, w_sigma)
        b_dist = dist.Normal(b_mu, b_sigma)

        variational = {'encoder.0.weight': w_dist, 'encoder.0.bias': b_dist}

        lifted_module = pyro.random_module("module", self.encoder, variational)
        return lifted_module()

    def predict_with_uncertainty(self, x, n_samples=100):
        """Monte Carlo dropout for uncertainty estimation"""
        predictions = []

        for _ in range(n_samples):
            with torch.no_grad():
                pred = self.encoder(x)
                predictions.append(pred)

        predictions = torch.stack(predictions)
        mean_pred = predictions.mean(0)
        std_pred = predictions.std(0)

        return mean_pred, std_pred

While experimenting with Bayesian methods, I discovered that uncertainty estimates were particularly valuable for prioritizing anomaly investigations in operational scenarios.

Real-World Applications: Satellite Operations Center Integration

Automated Anomaly Triage System

Based on my hands-on experience with satellite operations, I developed an integrated anomaly triage system:

class SatelliteAnomalyTriage:
    def __init__(self, model, confidence_threshold=0.85):
        self.model = model
        self.confidence_threshold = confidence_threshold
        self.anomaly_db = self._initialize_database()

    def process_telemetry_stream(self, telemetry_data, metadata):
        """Real-time anomaly detection and triage"""
        anomalies = []

        for satellite_id, data in telemetry_data.items():
            # Extract temporal features
            features = self._extract_temporal_features(data)

            # Get anomaly scores with uncertainty
            scores, uncertainty = self.model.predict_with_uncertainty(features)

            # Identify high-confidence anomalies
            high_conf_mask = (scores > 0.7) & (uncertainty < 0.2)
            high_conf_anomalies = scores[high_conf_mask]

            if len(high_conf_anomalies) > 0:
                # Classify anomaly type
                anomaly_type = self._classify_anomaly(
                    data, high_conf_anomalies
                )

                # Assess severity
                severity = self._assess_severity(
                    anomaly_type,
                    high_conf_anomalies.mean()
                )

                # Generate response plan
                response_plan = self._generate_response_plan(
                    satellite_id,
                    anomaly_type,
                    severity,
                    metadata
                )

                anomalies.append({
                    'satellite_id': satellite_id,
                    'anomaly_type': anomaly_type,
                    'severity': severity,
                    'confidence': 1 - uncertainty.mean().item(),
                    'response_plan': response_plan,
                    'timestamp': metadata['timestamp']
                })

        return anomalies

    def _classify_anomaly(self, data, anomaly_scores):
        """Classify anomaly using learned temporal patterns"""
        # Extract pattern signatures
        pattern_features = self._extract_pattern_signatures(data)

        # Compare with known anomaly patterns
        similarities = self._compute_pattern_similarity(pattern_features)

        # Return most similar anomaly type
        return self.anomaly_db.iloc[similarities.argmax()]['anomaly_type']

    def _generate_response_plan(self, satellite_id, anomaly_type, severity, metadata):
        """Generate automated response recommendations"""
        # Query historical responses for similar anomalies
        historical_responses = self._query_historical_responses(
            anomaly_type, severity
        )

        # Consider current operational context
        context = self._assess_operational_context(metadata)

        # Generate prioritized action list
        actions = self._prioritize_actions(
            historical_responses,
            context,
            satellite_id
        )

        return {
            'immediate_actions': actions['immediate'],
            'short_term_actions': actions['short_term'],
            'long_term_actions': actions['long_term'],
            'estimated_risk': self._estimate_risk(severity, context)
        }

During my investigation of operational integration, I found that providing confidence scores and recommended actions significantly improved operator response times.

Challenges and Solutions

Challenge 1: Non-Stationary Satellite Behavior

One significant problem I encountered was that satellites don’t behave consistently over their lifetime. Components degrade, software updates change behavior, and orbital adjustments alter thermal profiles.

Solution: I implemented an adaptive normalization scheme:


python
class AdaptiveNormalization:
    def __init__(self, window_size=1000, adaptation_rate=0.01):
        self.window_size = window_size
        self.rate = adaptation_rate
        self.running_stats = {}

    def update_and_normalize(self, satellite_id, new_data):
        """Adaptive normalization with concept drift detection"""
        if satellite_id not in self.running_stats:
            self.running_stats[satellite_id] = {
                'mean': new_data.mean(0),
                'std': new_data.std(0),
                'buffer': []
            }

        stats = self.running_stats[satellite_id]

        # Detect concept drift
        drift_score = self._detect_drift(new_data, stats)

        if drift_score > 0.1:  # Significant drift detected
            # Update statistics with adaptation
            new_mean = (1 - self.rate) * stats['mean'] + self.rate * new_data.mean(0)
            new_std = (1 - self.rate) * stats['std'] + self.rate * new_data.std(0)

            stats['mean'] = new_mean
            stats['std'] = new_std

            # Clear buffer for new regime
            stats['buffer'] = []

        # Update buffer
        stats['buffer'].append(new_data)
        if len(stats['buffer']) > self.window_size:
            stats['buffer'].pop(0)

        # Normalize data
        normalized = (new_data - stats['mean']) / (stats['std'] + 1e-8)

        return normalized, drift_score

    def _detect_drift(self, new_data, stats):
        """KL divergence-based drift detection"""
        # Compute distribution statistics
        current_mean = stats['mean']
        current_std = stats['std']

        # Compute KL divergence between distributions
        kl_div = self._compute_kl_diverg

How to Search Secretary of State Business Filings Programmatically (Multi-State)

If you work in KYC, compliance, sales, or due diligence, you’ve probably spent time on state Secretary of State websites manually searching for business entities. The experience is universally terrible — each state has a different system, most are built on 2000s-era ASP.NET, and none of them talk to each other.

There’s no national business entity database. Each of the 50 states maintains its own registry independently. If you need to verify that “Acme LLC” is registered and active, you have to know which state it’s registered in and search that state’s specific portal.

Here’s what I’ve learned building tools to automate this across multiple states.

The state-by-state landscape

California

  • Portal: bizfileonline.sos.ca.gov
  • Searchable by: entity name, entity number
  • Data available: entity name, number, type, status, formation date, jurisdiction, agent info
  • API: CA has an unofficial API that returns JSON. It’s the cleanest of any state.

Texas

  • Portal: SOSDirect (sos.state.tx.us)
  • Searchable by: entity name, entity number, registered agent, officer/director name
  • Data available: entity name, type, status, formation date, registered agent, officers
  • API: No public API. The portal is a classic ASP.NET form with ViewState.

New York

  • Portal: appext20.dos.ny.gov/corp_public
  • Searchable by: entity name, DOS ID
  • Data available: entity name, type, status, jurisdiction, filing date, county, process address
  • API: No public API. Returns HTML tables.

Florida

  • Portal: search.sunbiz.org
  • Searchable by: entity name, officer/agent name, document number
  • Data available: entity name, type, status, filing date, officers, registered agent, annual reports
  • API: No API, but FL publishes bulk data via SFTP (weekly). This is the exception.

The common problems

1. No standard schema. California calls it “Entity Number”, Texas calls it “Filing Number”, New York calls it “DOS ID”, Florida calls it “Document Number”. Same concept, different field name, different format.

2. Anti-bot protection is increasing. As of 2026, Florida and several other states have added Cloudflare or Akamai protection. Texas and New York still work with direct HTTP requests. California’s unofficial API is the easiest to work with.

3. Search is fuzzy in unhelpful ways. Some states return exact matches only. Others return wildcard matches that bury what you’re looking for in hundreds of results. Name formatting (LLC vs L.L.C. vs Limited Liability Company) causes missed matches.

4. Rate limits are invisible. No state documents their rate limits. Hit them too fast and you get soft-blocked (empty results or 503 errors) without any error message explaining why.

Building it yourself vs. using existing tools

If you need to search one state occasionally, the web portal is fine. If you need to:

  • Search across multiple states in a single workflow
  • Do bulk entity verification (KYC/compliance)
  • Monitor entity status changes over time
  • Feed results into a CRM or database automatically

…then you need programmatic access.

I built individual actors for each state that handle the portal differences, anti-bot measures, and output normalization:

Each one takes a search query and returns normalized JSON:

{
  "entityName": "ACME HOLDINGS LLC",
  "entityNumber": "202312345678",
  "entityType": "Limited Liability Company",
  "status": "Active",
  "formationDate": "2023-05-15",
  "jurisdiction": "California",
  "registeredAgent": "CT Corporation System",
  "principalAddress": "123 Market St, San Francisco, CA 94105"
}

Why fragmentation is the real problem (and the opportunity)

The reason no one has built a clean multi-state business entity search is that it’s 50 separate engineering problems. Each state has different:

  • Web technology (ASP.NET, PHP, Java servlets, React)
  • Search parameters and result formats
  • Anti-bot measures
  • Data fields and naming conventions
  • Update frequency and data freshness

Services like OpenCorporates, Cobalt Intelligence, and LexisNexis aggregate this data but charge enterprise pricing ($500-5000/month). If you need occasional lookups or want to build your own pipeline, the per-state tools above cost pennies per search.

What’s coming

I’m expanding to cover all 50 states. The top 5 by entity count (CA, TX, NY, FL, IL) are done. Next: IL, PA, OH, GA, NJ. The goal is programmatic access to every state’s business entity registry with normalized output.

If you’re building KYC/compliance tooling and need specific states, drop a comment — I’ll prioritize based on demand.

I build automation tools for public records data. State SOS portals are my specialty — and my recurring nightmare.

Complete Guide to Building SaaS with Next.js and Supabase

Complete Guide to Building SaaS with Next.js and Supabase

Building a Software-as-a-Service (SaaS) application requires mastering multiple technologies and architectural patterns. This comprehensive guide walks you through building production-ready SaaS applications using Next.js 15 and Supabase—a powerful combination that enables indie developers to ship fast without sacrificing quality.

Why Next.js + Supabase for SaaS?

Next.js 15 provides:

  • Server-side rendering and static generation
  • API routes for backend logic
  • Optimized performance out of the box
  • Edge runtime support
  • Built-in TypeScript support

Supabase offers:

  • PostgreSQL database with real-time capabilities
  • Built-in authentication
  • Row-level security (RLS)
  • Storage for files and media
  • Auto-generated APIs

Together, they form a complete stack that lets you focus on building features instead of infrastructure.

1. Project Setup and Configuration

Initial Setup

## Create Next.js app
npx create-next-app@latest my-saas-app --typescript --tailwind --app

## Install Supabase
npm install @supabase/supabase-js @supabase/ssr

Environment Configuration

## .env.local
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Supabase Client Setup

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

2. Database Design for SaaS

Multi-Tenant Architecture

Design your database schema to support multiple customers (tenants):

-- Organizations table
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Users table
CREATE TABLE users (
  id UUID PRIMARY KEY REFERENCES auth.users,
  email TEXT NOT NULL,
  full_name TEXT,
  avatar_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Organization members
CREATE TABLE organization_members (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(organization_id, user_id)
);

Row-Level Security (RLS)

Protect your data with RLS policies:

-- Enable RLS
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;

-- Users can only see organizations they're members of
CREATE POLICY "Users can view their organizations"
  ON organizations FOR SELECT
  USING (
    id IN (
      SELECT organization_id 
      FROM organization_members 
      WHERE user_id = auth.uid()
    )
  );

Related: Supabase Database Schema Design for SaaS Applications, Implement Soft Deletes in Supabase PostgreSQL

3. Authentication and Authorization

Email/Password Authentication

// app/auth/login/page.tsx
'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const supabase = createClient()

  async function handleLogin(e: React.FormEvent) {
    e.preventDefault()

    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })

    if (error) {
      console.error('Login error:', error.message)
      return
    }

    // Redirect to dashboard
    window.location.href = '/dashboard'
  }

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit">Sign In</button>
    </form>
  )
}

Social Authentication

// Sign in with Google
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
})

Protected Routes

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  const response = NextResponse.next()

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        set(name: string, value: string, options: any) {
          response.cookies.set({ name, value, ...options })
        },
        remove(name: string, options: any) {
          response.cookies.set({ name, value: '', ...options })
        },
      },
    }
  )

  const { data: { session } } = await supabase.auth.getSession()

  if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/auth/login', request.url))
  }

  return response
}

export const config = {
  matcher: ['/dashboard/:path*'],
}

Related: Implement Social Auth with Supabase Next.js Complete Guide, Build Protected Routes in Next.js 15 with Supabase

4. API Development

REST API with Route Handlers

// app/api/organizations/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET() {
  const supabase = createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { data, error } = await supabase
    .from('organizations')
    .select(`
      *,
      organization_members!inner(role)
    `)
    .eq('organization_members.user_id', user.id)

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ data })
}

export async function POST(request: Request) {
  const supabase = createClient()
  const { name, slug } = await request.json()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Create organization
  const { data: org, error: orgError } = await supabase
    .from('organizations')
    .insert({ name, slug })
    .select()
    .single()

  if (orgError) {
    return NextResponse.json({ error: orgError.message }, { status: 500 })
  }

  // Add user as owner
  const { error: memberError } = await supabase
    .from('organization_members')
    .insert({
      organization_id: org.id,
      user_id: user.id,
      role: 'owner',
    })

  if (memberError) {
    return NextResponse.json({ error: memberError.message }, { status: 500 })
  }

  return NextResponse.json({ data: org }, { status: 201 })
}

Server Actions

// app/actions/organizations.ts
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function createOrganization(formData: FormData) {
  const supabase = createClient()

  const name = formData.get('name') as string
  const slug = formData.get('slug') as string

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return { error: 'Unauthorized' }
  }

  const { data, error } = await supabase
    .from('organizations')
    .insert({ name, slug })
    .select()
    .single()

  if (error) {
    return { error: error.message }
  }

  revalidatePath('/dashboard/organizations')
  return { data }
}

Related: Build REST API with Next.js 15 Route Handlers, Implement API Rate Limiting in Next.js Applications

5. Real-time Features

Real-time Subscriptions

'use client'

import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'

export function RealtimeMessages({ channelId }: { channelId: string }) {
  const [messages, setMessages] = useState<any[]>([])
  const supabase = createClient()

  useEffect(() => {
    // Fetch initial messages
    supabase
      .from('messages')
      .select('*')
      .eq('channel_id', channelId)
      .order('created_at', { ascending: true })
      .then(({ data }) => setMessages(data || []))

    // Subscribe to new messages
    const channel = supabase
      .channel(`messages:${channelId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `channel_id=eq.${channelId}`,
        },
        (payload) => {
          setMessages((current) => [...current, payload.new])
        }
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [channelId])

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>{message.content}</div>
      ))}
    </div>
  )
}

Related: Implement Supabase Realtime Subscriptions in Next.js 15, Build Real-Time Collaboration Features with Supabase

6. File Storage

Upload Files to Supabase Storage

'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'

export function FileUpload() {
  const [uploading, setUploading] = useState(false)
  const supabase = createClient()

  async function uploadFile(event: React.ChangeEvent<HTMLInputElement>) {
    try {
      setUploading(true)

      if (!event.target.files || event.target.files.length === 0) {
        throw new Error('You must select a file to upload.')
      }

      const file = event.target.files[0]
      const fileExt = file.name.split('.').pop()
      const fileName = `${Math.random()}.${fileExt}`
      const filePath = `${fileName}`

      const { error: uploadError } = await supabase.storage
        .from('uploads')
        .upload(filePath, file)

      if (uploadError) {
        throw uploadError
      }

      // Get public URL
      const { data } = supabase.storage
        .from('uploads')
        .getPublicUrl(filePath)

      console.log('File uploaded:', data.publicUrl)
    } catch (error) {
      alert('Error uploading file!')
      console.log(error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div>
      <input
        type="file"
        onChange={uploadFile}
        disabled={uploading}
      />
    </div>
  )
}

Related: Build File Upload with Progress Bar Supabase Next.js, Supabase Storage CDN Configuration for Next.js

7. Deployment and Scaling

Deploy to Vercel

## Install Vercel CLI
npm i -g vercel

## Deploy
vercel --prod

Environment Variables

Set these in Vercel dashboard:

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
  • SUPABASE_SERVICE_ROLE_KEY

Database Migrations

## Install Supabase CLI
npm install supabase --save-dev

## Create migration
npx supabase migration new add_organizations

## Apply migrations
npx supabase db push

Related: Deploy Next.js Supabase App to Vercel Production, Handle Database Migrations in Production Supabase

8. Monitoring and Optimization

Error Tracking with Sentry

// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs"

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
})

Performance Monitoring

// lib/analytics.ts
export function trackEvent(name: string, properties?: Record<string, any>) {
  if (typeof window !== 'undefined') {
    // Track with your analytics provider
    console.log('Event:', name, properties)
  }
}

Related: Monitor Next.js Application Performance in Production, Set Up Error Tracking with Sentry in Next.js

9. Common Pitfalls and Solutions

Session Management Issues

Problem: Auth session not persisting after refresh

Solution: Use server-side session management with cookies

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export function createClient() {
  const cookieStore = cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
      },
    }
  )
}

RLS Policy Errors

Problem: Users can’t access their own data

Solution: Check RLS policies and ensure auth.uid() is used correctly

-- Debug RLS policies
SELECT * FROM pg_policies WHERE tablename = 'organizations';

-- Test as specific user
SET request.jwt.claim.sub = 'user-uuid-here';
SELECT * FROM organizations;

Performance Issues

Problem: Slow database queries

Solution: Add indexes and optimize queries

-- Add index on foreign keys
CREATE INDEX idx_organization_members_user_id 
  ON organization_members(user_id);

CREATE INDEX idx_organization_members_org_id 
  ON organization_members(organization_id);

10. Next Steps

Now that you have a solid foundation, explore these advanced topics:

  • Implement Optimistic UI Updates in Next.js with Supabase
  • Add Full-Text Search to Next.js App with Supabase
  • Build Advanced Filtering System with Supabase
  • Implement Vector Search with Supabase pgvector
  • Add AI-Powered Features to Your SaaS

Frequently Asked Questions (FAQ)

What is the best tech stack for building a SaaS application?

Next.js 15 with Supabase is an excellent choice for SaaS applications. Next.js provides server-side rendering, API routes, and optimized performance, while Supabase offers PostgreSQL database, authentication, real-time capabilities, and storage—all without managing infrastructure.

How do I implement multi-tenancy in Supabase?

Create an organizations table and organization_members junction table. Use Row Level Security (RLS) policies to ensure users only access data from organizations they belong to. Filter queries using organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()).

Should I use Server Components or Client Components for my SaaS?

Use Server Components by default for better performance and smaller bundle sizes. Only use Client Components when you need interactivity (forms, buttons), React hooks (useState, useEffect), or browser APIs. This can reduce your JavaScript bundle by 50%+.

How do I handle user authentication in a SaaS application?

Use Supabase Auth with email/password or social OAuth providers. Implement middleware to protect routes, create server and client Supabase clients for different contexts, and use RLS policies to secure data access at the database level.

What’s the best way to structure my database for SaaS?

Design for multi-tenancy from the start with organizations, users, and organization_members tables. Enable RLS on all tables, create proper indexes on foreign keys, and use database functions for complex operations that need transactions.

How do I implement role-based access control (RBAC)?

Create a roles table with permissions stored as JSONB, a user_roles junction table, and database functions to check permissions. Use these functions in RLS policies: CREATE POLICY ... USING (has_permission('resource.action')).

Should I use API routes or Server Actions?

Use Server Actions for form submissions and mutations—they’re simpler and more performant. Use API routes when you need RESTful endpoints for external integrations, webhooks, or when you need fine-grained control over HTTP responses.

How do I handle file uploads in my SaaS?

Use Supabase Storage with the Next.js file upload API. Create storage buckets with RLS policies, implement upload progress tracking, and use Supabase’s image transformation API for optimized delivery. Store file metadata in your database.

What’s the best deployment platform for Next.js + Supabase?

Vercel is the recommended platform—it’s built by the Next.js team, offers zero-config deployment, automatic HTTPS, preview deployments, and edge network distribution. The free tier is generous for indie developers and startups.

How do I implement real-time features?

Use Supabase Realtime to subscribe to database changes, presence tracking for online users, and broadcast channels for ephemeral data like typing indicators. Always clean up subscriptions when components unmount to prevent memory leaks.

Should I use TypeScript for my SaaS application?

Yes, TypeScript catches errors at compile time, provides better IDE support, and makes refactoring safer. Generate types from your Supabase database with npx supabase gen types typescript to keep your types in sync with your schema.

How do I handle database migrations in production?

Use Supabase CLI to create and manage migrations. Test migrations in staging first, use transactions for data migrations, and always create rollback migrations. Never modify the database schema directly in production.

Related Articles

Conclusion

Building a SaaS application with Next.js and Supabase gives you a powerful, scalable foundation. Focus on shipping features quickly, iterate based on user feedback, and scale as you grow.

The combination of Next.js’s performance and Supabase’s backend capabilities means you can build production-ready applications without managing infrastructure—perfect for indie developers and small teams.

Start building today, and remember: ship fast, iterate faster.

Originally published at https://iloveblogs.blog

How to Search the NPI Registry for Healthcare Providers (Without the Terrible NPPES Website)

If you’ve ever needed to look up a doctor’s NPI number, verify a provider’s credentials, or build a list of specialists in a given area, you’ve probably used the NPPES website. And you’ve probably noticed it’s awful.

The search is slow, the results are paginated one painful page at a time, there’s no bulk export, and the interface looks like it hasn’t been updated since 2008. Which it might not have been.

The good news: there’s a free public API behind it that’s actually pretty decent. The bad news: the docs are scattered and the response format is… a lot.

What’s in the NPI Registry?

Every healthcare provider in the US that bills insurance has a National Provider Identifier — a 10-digit number assigned by CMS. The NPPES registry has over 7 million active records.

For individual providers (doctors, nurses, therapists, dentists), you get:

  • Full name and credentials (MD, DO, NP, LCSW, etc.)
  • Primary specialty and all taxonomy codes
  • Practice address and phone number
  • Mailing address
  • License numbers by state
  • Enumeration date (when they got their NPI)

For organizations (hospitals, clinics, group practices), you get the organization name, authorized official, address, specialties, and the same taxonomy data.

Who actually needs this data?

More people than you’d think. I’ve talked to folks in healthcare sales, credentialing, insurance verification, and compliance who all need NPI lookups at scale. A few common use cases:

Healthcare sales teams building prospect lists of cardiologists in Texas or dentists in the Bay Area. The NPI registry is basically a free healthcare provider directory with verified contact info.

Credentialing departments verifying that a provider is active, has the right specialty, and practices where they say they do. Manual lookups on the NPPES site take forever when you’re processing hundreds of applications.

Insurance companies and billing teams validating NPI numbers on claims. A wrong NPI means a rejected claim, and nobody wants to chase that down manually.

Researchers studying provider distribution, specialty density by geography, or healthcare access patterns. The data is rich enough for serious analysis if you can get it in bulk.

The NPPES API

CMS runs a free API at https://npiregistry.cms.hhs.gov/api/. No API key needed, no authentication, no signup. Just hit the endpoint.

Here’s a basic search for internal medicine doctors named Smith in California:

GET https://npiregistry.cms.hhs.gov/api/?version=2.1&first_name=Smith&enumeration_type=NPI-1&state=CA&taxonomy_description=Internal%20Medicine&limit=50

The response is JSON with a results array. Each result has nested objects for basic info, addresses, taxonomies, and other names.

The catch: the API maxes out at 200 results per request, and there’s no cursor or offset pagination. If your query matches more than 200 providers, you need to narrow your search (add city, postal code, etc.) or break it into multiple requests.

Also, the response structure is deeply nested. Addresses come in an array with different address_purpose values (“LOCATION” vs “MAILING”). Taxonomies are another array where you need to find the one with primary: true. It’s not hard, but it’s tedious to flatten into something usable.

The easy way

I built a tool that wraps the NPPES API, handles the pagination workaround, flattens the nested response into clean records, and exports to CSV/JSON/Excel:

NPPES NPI Lookup – Healthcare Provider Search

You search by name, NPI number, specialty, city, state, or ZIP code. It returns flat records with the fields you actually care about — name, credential, specialty, practice address, phone, NPI, status.

What it does that the raw API doesn’t

  • Handles the 200-result limit by breaking large queries into smaller geographic chunks
  • Flattens the nested response so you get one row per provider with clean columns
  • Returns up to 10,000 results per run
  • Exports directly to CSV, JSON, or Excel — no post-processing needed

Example: find all cardiologists in Houston

Set search type to “Individual Provider,” leave name blank, set specialty to “Cardiology,” city to “Houston,” state to “TX.” Run it. You get a clean spreadsheet of every cardiologist in Houston with their NPI, practice address, phone number, and credentials.

Pricing

$0.005 per result + $0.10 per run. 1,000 provider records costs about $5.10. No subscription, no monthly minimum.

Tips if you’re building your own

If you want to hit the API directly, a few things that tripped me up:

The taxonomy_description field is fuzzy-matched. Searching for “cardio” returns Cardiology, Cardiovascular Disease, Cardiovascular Surgery, etc. This is actually useful for discovery but annoying when you want exact matches.

Use enumeration_type to filter. NPI-1 is individual providers, NPI-2 is organizations. If you don’t specify, you get both mixed together.

The limit parameter caps at 200. And there’s no skip or offset. If you need more than 200 results, you have to subdivide your search. State + city combinations work well for this.

Rate limiting is generous but undocumented. I’ve hit it at around 2-3 requests per second. Adding a small delay between requests keeps things stable. The API doesn’t return rate limit headers, so you won’t know you’re hitting the limit until requests start failing.

Watch for deactivated NPIs. Some records have status: "D" (deactivated). If you’re building a prospect list or doing credentialing verification, filter these out.

Bulk download alternative

CMS also publishes a monthly full export of the NPI registry as a CSV file. It’s about 8GB uncompressed. If you need the entire dataset and don’t mind working with a massive file, that’s at https://download.cms.gov/nppes/NPI_Files.html.

For most use cases though, searching by criteria is more practical than downloading 7 million records and filtering locally.

If you’re doing healthcare provider lookups at scale, try the NPI Lookup tool on Apify. Questions or feature requests — hit me up on the Apify Discord or open an issue on the actor page.

How to Search SEC EDGAR Filings by Ticker, Company Name, or CIK (Without Paying for an API)

Universal field group settings UI

If you’ve ever tried to pull SEC filing data programmatically, you know the pain. The EDGAR full-text search is clunky, the submissions API returns deeply nested JSON that’s annoying to parse, and the rate limiting (10 req/s with a mandatory User-Agent) means you can’t just hammer it from a script.

The commercial options aren’t cheap either. sec-api.io starts at $49/mo. Quandl (now Nasdaq Data Link) is $29/mo for basic access. Financial Modeling Prep charges per call. If you just need filing metadata and don’t want to build a whole pipeline, that’s a lot of overhead.

I built a free actor on Apify that wraps the EDGAR API and gives you clean, flat JSON for any company lookup. No API key needed. You pay per result on Apify ($0.005/company) and it handles the pagination, rate limiting, and data flattening.

What it actually returns

For each company, you get: CIK, ticker, SIC code (industry classification), EIN, state of incorporation, filer category, phone number, business and mailing address, former names, and every filing matching your filters.

Each filing record has the form type, filing date, report date, accession number, direct document URL, filing index URL, XBRL availability flag, and file size. So you can go straight from search result to the actual document.

Quick examples

Pull 10-K annual reports for FAANG stocks:

{
    "tickers": ["AAPL", "AMZN", "GOOGL", "META", "NFLX"],
    "formTypes": ["10-K"],
    "maxFilings": 5
}

Track insider trading at NVIDIA since 2025:

{
    "tickers": ["NVDA"],
    "formTypes": ["4"],
    "dateFrom": "2025-01-01",
    "maxFilings": 100
}

Search by company name (partial match works):

{
    "companyNames": ["Palantir"],
    "formTypes": ["10-K", "10-Q", "8-K"],
    "maxFilings": 20
}

Results come back in 2-3 seconds per company. Export as JSON, CSV, or Excel.

Why not just use the raw EDGAR API?

You can. It’s free and public. But here’s what you’d need to build:

  1. Company resolution — EDGAR identifies companies by CIK, not ticker. You’d need to maintain a ticker-to-CIK mapping or hit the company search endpoint first.

  2. Pagination — The submissions endpoint returns filings in batches across multiple URLs. Recent filings are inline, older ones are in separate CIK0001234567-submissions-001.json files you have to fetch and merge.

  3. Rate limiting — 10 requests per second, and the SEC blocks you (temporarily) if you don’t include a valid User-Agent with your email. Most tutorials forget to mention this.

  4. Data flattening — The raw JSON nests filings inside recentFilings and files arrays with parallel arrays for each field (one array for dates, one for accession numbers, etc). Not the worst API design I’ve seen, but close.

None of this is hard, but it’s 2-4 hours of work for something that should take 10 seconds.

Who actually uses this

From what I can tell, the main use cases are:

  • Investment analysts who need filing history for fundamental research without paying for a Bloomberg terminal
  • Compliance teams monitoring 8-K events and insider trading disclosures
  • Due diligence workflows where you need complete filing history for acquisition targets
  • Academic researchers building datasets of financial disclosures
  • Lead gen — every public company’s phone number and address is in EDGAR, filterable by industry (SIC code)

The lead gen angle surprised me, but it makes sense. EDGAR has structured contact data for 10,400+ companies, organized by industry, with filing frequency as a proxy for company activity.

Try it

The actor is here: SEC EDGAR Company Filings Search

It’s part of a collection of 30 public data tools I built covering government APIs, business registrations, and regulatory data. All free to try, all on Apify, all written in Go.

If you work with any of these data sources — SEC filings, IRS 990 nonprofits, FDA drug safety, state business registrations, federal contracts — there’s probably an actor in the collection that saves you a few hours of API wrangling.

When Synthetic Data Lies: A Hidden Correlation Problem I Didn’t Expect

While working on a small analytics setup using ClickHouse and Superset, I generated some synthetic data to test queries and dashboards.

Initially, everything looked fine. The distributions seemed reasonable, and the dashboards behaved as expected.

But as I increased the dataset size, a few patterns started to look off.

Revenue seemed to concentrate in a single country.
In some cases, certain countries had no purchases at all.

At first, it looked like a simple distribution issue. But the patterns were too consistent to ignore.

Checking the Usual Suspects

The first assumption was that something was wrong with the queries or aggregations.

So I checked:

  • query logic
  • filters
  • materialized views
  • dashboard configurations

Everything seemed correct.

Which pointed to a different possibility:

The issue wasn’t in how the data was queried – it was in how the data was generated.

Looking at the Data More Closely

Instead of relying on dashboards, I went back to the raw data.

A simple aggregation made things clearer:

  • one country dominating purchases
  • another missing entirely


Overall event distribution looks reasonable – the issue isn’t obvious here

At this point, it was clear that the data itself wasn’t behaving as expected.

The First Issue: Randomness That Wasn’t Quite Random

The initial data generation logic used rand() like this:

multiIf(
    rand() % 100 < 40, 'India',
    rand() % 100 < 65, 'US',
    rand() % 100 < 80, 'UK',
    rand() % 100 < 90, 'Germany',
    'UAE'
)

At a glance, this looks reasonable.

But each rand() call is evaluated independently.

So instead of generating a single random value and assigning a category, the logic evaluates a new random value at each step.

This leads to unintended distributions and subtle bias in the data.

Fixing That… and Introducing Another Problem

To make the data more stable, I switched to a deterministic approach using number:

(number * 17) % 100 AS event_rand
(number * 29) % 100 AS country_rand

This made the distributions predictable and easier to reason about.


After fixing randomness – a different issue appears (some countries have zero purchases)

But it introduced a different issue.

Both event_type and country were now derived from the same base value: number.

The Real Issue: Hidden Correlation

Even with different multipliers, these values were not independent.

They were mathematically related.

This meant:

  • certain rows could only produce certain combinations
  • some combinations would never occur

In this case, the rows that produced "purchase" never aligned with the rows that produced "UAE".

Which resulted in:

  • UAE having zero purchases
  • other countries showing skewed distributions

What Was Actually Wrong

The issue wasn’t randomness.

It was lack of independence.

The variables in the synthetic dataset were not independent of each other.

And that’s enough to produce misleading analytics.

Fixing the Data Generation

To resolve this, I changed how the values were generated:

  • used different transformations
  • added offsets
  • ensured each variable had its own distribution

For example:

(number * 17) % 100 AS event_rand
(number * 31 + 13) % 100 AS country_rand
(number * 47 + 23) % 100 AS device_rand

This breaks the alignment between variables and restores independence.


After this change, the distributions behaved as expected.

Why This Matters

At smaller scales, the issue wasn’t obvious.

The data looked fine, and the dashboards seemed reasonable.

But as the dataset grew:

  • patterns became more consistent
  • biases became more visible
  • incorrect assumptions started to look like real insights

Key Takeaway

Synthetic data can look correct while still producing misleading results.

The problem wasn’t query performance.

It was data correctness.

Scaling the data didn’t create the issue.

It revealed it.

Final Thoughts

This was a good reminder that:

  • data generation deserves as much attention as querying
  • small assumptions can lead to large inconsistencies
  • and “reasonable-looking” data isn’t always reliable

When working with analytics systems, it’s easy to trust what the data shows.

But sometimes, it’s worth questioning how that data was created in the first place.

Related

This came up while building an analytics setup using ClickHouse and Superset, where I was comparing raw tables and materialized views.

If you’re interested in that setup, you can read about it here:
👉 link to Blog 1