The Open Web Is Closing, So Build Your Own Door

A paper aeroplane with the word SPAM written on it in big letters, to celebrate the release of the Circuspam Free Spam Toolkit.

There was a time when finding someone’s email felt like a handshake—an introduction, not a transaction. You built something, you reached out, you started a conversation. That was the web. Now, the toll booths have multiplied: every useful API hides behind credit cards, every inbox is “managed” by algorithms that decide which voices deserve oxygen, and every social platform charges rent for the privilege of being seen. (Circuspam is releasing a free email marketing dataset generator for the Everyman!)

Small teams, solo scientists, indie devs, and garage startups don’t have £500 a month for a Salesforce starter pack. They have ideas, early prototypes, and the audacity to think someone might care. But the infrastructure of outreach has been fenced off. “Spam” is the word they use to describe unlicensed communication now, conveniently forgetting that spam filters have become gatekeepers protecting incumbents from competition.

It doesn’t have to be this way.

Today, Circuspam is releasing the FREE SPAM TOOLKIT—a dead-simple, three-stage pipeline that turns public UK company data into a clean, usable marketing dataset. Not only that, but there will be no GDPR headaches (this is B2B; data protection law doesn’t apply to corporate entities). No enterprise contracts. Just Python, three free API tiers, and the revolutionary act of sending email without paying a middleman.

How It Works

  1. Companies House – Queries the UK’s open registry for active companies in your chosen industries (software, biotech, data, whatever). Pulls directors, SIC codes, and addresses.
  2. Groq Compound-Mini – Uses Groq’s native web-search model to find the company’s actual domain from just the company name. No manual hunting.
  3. Hunter.io – Cross-references the domain for publicly listed email patterns. Free tier gives you enough to start real conversations.

The script handles rate limits, retries, and packages everything into a CSV you can actually use. It can be hit-and-miss, but sometimes you will get 10+ contact emails for your email marketing list, that you know are in your niche because the companies are in your niche.

Installation & Usage

pip install groq requests pandas tenacity

Save the script as circuspam_toolkit.py, add your free API keys to the top, configure your industries, and run:

python circuspam_toolkit.py
from groq import Groq
import requests
import pandas as pd
import base64
import time
from tenacity import retry, wait_random_exponential, stop_after_attempt, retry_if_exception_type

# YOUR API KEYS HERE
COMPANIES_HOUSE_API_KEY = "AAA"
GROQ_API_KEY = "AAA"
HUNTER_KEY = "AAA"

# Configure Industries and save file
industries = ["software", "biotechnology", "data", "tech"]
save_file_name = "companies.csv"

# Rest of code below
COMPANIES_HOUSE_BASE_URL = "https://api.company-information.service.gov.uk"

client = Groq(api_key=GROQ_API_KEY)

auth_str = f"{COMPANIES_HOUSE_API_KEY}:"
auth_b64 = base64.b64encode(auth_str.encode()).decode()
headers = {"Authorization": f"Basic {auth_b64}"}


def return_companies():
query_string = "+OR+".join(industries)
search_url = f"{COMPANIES_HOUSE_BASE_URL}/search/companies?q={query_string}&items_per_page=100"
resp = requests.get(search_url, headers=headers)
data = resp.json()

companies = []
for item in data.get('items', []):
if item.get('company_status') == 'active':
company_num = item.get('company_number')

# Company profile
profile_url = f"{COMPANIES_HOUSE_BASE_URL}/company/{company_num}"
profile_resp = requests.get(profile_url, headers=headers)
profile = profile_resp.json() if profile_resp.status_code == 200 else {}

# Officers (directors) - where emails hide
officers_url = f"{COMPANIES_HOUSE_BASE_URL}/company/{company_num}/officers"
officers_resp = requests.get(officers_url, headers=headers)
officers = officers_resp.json().get('items',
[]) if officers_resp.status_code == 200 else []

company_data = {
'name': item.get('title', ''),
'number': company_num,
'status': item.get('company_status', ''),
'address': profile.get('registered_office_address', {}).get(
'address_snippet', ''),
'sic': ', '.join(profile.get('sic_codes', [])),
'email': '',
'phone': profile.get('links', {}).get('self', '').split('@')[
-1] if '@' in str(
profile.get('links', {})) else '',
}

# Check officers for emails
for officer in officers[:3]: # Top 3 directors
if 'email' in str(officer).lower():
company_data['email'] = officer.get('email_address',
company_data['email'])

companies.append(company_data)
time.sleep(0.1) # Rate limit

return companies


class RateLimitError(Exception):
pass


@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(5),
retry=retry_if_exception_type(RateLimitError)
)
def get_domain_from_company(name: str) -> str | None:
"""Uses Groq Compound's native web search to find a domain."""
prompt = (
f"Search for the official website of the UK company '{name}'. "
f"Return ONLY the domain (e.g., example.com). If not found, return 'NONE'."
)

# Use the compound model which has native browsing/search tools
chat_completion = client.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model="groq/compound-mini", # Triggers native web search automatically
temperature=0.1,
max_tokens=30
)

content = chat_completion.choices[0].message.content.strip().lower()

# Simple extraction logic for the domain
if "none" in content or len(content) > 50:
return None
return content


def hunter_from_domain(domain: str) -> dict | None:
"""Standard Hunter.io domain search."""
url = "api.hunter.io"
params = {"domain": domain, "api_key": HUNTER_KEY, "limit": 1}
try:
r = requests.get(url, params=params)
if r.status_code == 200:
data = r.json().get("data", {})
emails = data.get("emails", [])
return emails[0] if emails else None
except:
return None
return None


if __name__ == "__main__":
print("Companies House Mailing List Generator (2 steps)\n")
print("1) Fetching companies from Companies House...")
df = pd.DataFrame(return_companies())
print(f"Saved {len(df)} companies with full details")
print(df[['name', 'email', 'address', 'sic']].head())

rows = []

print(f"2) Processing {len(df)} companies via Groq Native Search...\n")

for i, row in df.iterrows():
company = row["name"]
print(f"{i + 1:2d}/{len(df)}: {company}")

domain = get_domain_from_company(company)
if not domain:
print(" ❌ No domain found")
rows.append({"company": company, "domain": None, "email": None})
continue

print(f" 🔍 Domain: {domain}")
email_data = hunter_from_domain(domain)

if email_data:
email = email_data.get("value")
print(f" ✅ Found: {email}")
rows.append({"company": company, "domain": domain, "email": email})
else:
print(" ❌ No email found")
rows.append({"company": company, "domain": domain, "email": None})

time.sleep(2) # Protect rate limits

pd.DataFrame(rows).to_csv(save_file_name, index=False)
print(f"\nDone. Results saved to {save_file_name}")

What You’re Actually Getting

This free email marketing dataset generator, generates a CSV with company names, domains, and verified email patterns. Use it for investor outreach, beta tester recruitment, partnership proposals, or just introducing yourself to someone who should know you exist. GDPR applies to personal data; so the corporations are fair game. This is public information, assembled automatically, democratizing access to the same datasets that expensive SaaS platforms repackage with markup.

The Future Isn’t Paywalled

Yes, the open web is scarcer than it was. Yes, the giants have built very good walls. But walls have doors, and doors have handles. Tools like Groq’s compound models, open registries, and permissive APIs are lowering the cost of intelligence and automation to essentially zero. The individual creator isn’t dead—they’re just forced to be craftier. The next decade belongs to the scrappy, the technical, and the permissionless.

Build your list. Send your email. Start your thing.

Leave a Comment

Your email address will not be published. Required fields are marked *