If you've ever wanted to ship a niche paid newsletter but the data sourcing felt like a brick wall, this is the writeup for you. The combo of (1) earnings call transcripts as a structured source, (2) an LLM to summarize, and (3) a transactional email API takes a useful product from "I'd need a data analyst" to "I can prototype it Saturday afternoon".
Concrete shape: a free newsletter that emails subscribers a 200-word summary of every earnings call from a specific universe (Mag 7, or a single sector, or your home-country mid-caps), 1-2 minutes after the transcript drops. Convert to paid via a more thorough Pro tier (full transcript link, themes flagged, sector comparisons).
This is a real recipe people are running. Below is what they wire together.
The stack (4 pieces)
- EarningsCalls.dev — for the transcripts (Pro tier, $24.99/month, 5,000 requests/month is way more than you'll burn).
- Claude / GPT-4.1 — for the summary. ~$0.003 per call summary at current prices.
- Resend / Postmark — for transactional emails. Free tier covers your first 1,000+ subscribers.
- Beehiiv / Substack / Ghost — your front door + payment processor. Free tier on Beehiiv, no payment needed until you have ~2,500 free subs.
No infrastructure. No database. No queue. One scheduled function.
The minimum-viable code
import requests, openai
from datetime import datetime, timedelta
API_KEY = "ec_..." # your earningscalls.dev key
BASE = "https://earningscalls.dev/api/v1"
H = {"X-API-Key": API_KEY}
UNIVERSE = ["NVDA","MSFT","AAPL","GOOGL","META","AMZN","TSLA"]
def find_new_calls(since):
out = []
for t in UNIVERSE:
r = requests.get(f"{BASE}/companies/ticker/{t}/latest", headers=H).json()
call = r.get("data", {})
if call.get("event_date_time","") > since:
out.append(call)
return out
def transcript(call_id):
return requests.get(f"{BASE}/transcripts/{call_id}?format=full",
headers=H).json()["data"]["full_transcript_text"]
def summarize(text, company):
return openai.chat.completions.create(
model="gpt-4.1-mini",
messages=[{"role":"user","content":
f"Summarise this earnings call for {company} in 200 words. "
f"Hit: revenue, guidance, three key takeaways. Plain prose, no bullets.\n\n{text[:30000]}"
}]
).choices.message.content
def send(subject, body):
requests.post("https://api.resend.com/emails",
headers={"Authorization": f"Bearer {os.environ['RESEND']}"},
json={"from": "[email protected]",
"to": [s for s in your_subscribers()],
"subject": subject,
"html": markdown_to_html(body)})
# Run this every 15 minutes
since = (datetime.utcnow() - timedelta(minutes=15)).isoformat()
for call in find_new_calls(since):
txt = transcript(call["id"])
summary = summarize(txt, call["company_name"])
send(f"{call['company_name']} just reported — {call['transcript_title']}",
summary)
That's ~50 lines. Add error handling, a subscriber DB, an unsubscribe link, and HTML formatting and you're at 300.
Niches that work
The market for "yet another earnings newsletter" is saturated. Niches that aren't:
- One sector, deep: REIT earnings, regional banks, midstream energy. Specialist audiences pay.
- Cross-listed companies a major broker won't cover: Canadian small-caps, Australian REITs, Nordic SaaS. Their primary investor audience doesn't get good coverage in English.
- Theme-only: "Every mention of agentic AI on F500 calls this week." Theme readers will pay for the cut you save them.
- Sentiment delta: subscribers get notified only when sentiment moves meaningfully vs the company's trailing 4 quarters. High-signal, low-volume.
The narrower the niche, the higher the conversion to paid.
What it costs to run
| Item | Monthly |
|---|---|
| EarningsCalls.dev Pro | $24.99 |
| OpenAI / Anthropic (300 summaries × $0.003) | ~$1 |
| Resend (1,000 sends) | $0 |
| Beehiiv (free tier under 2,500 subs) | $0 |
| Total | ~$26 |
Push to $30–40k MRR is plausible if the niche is right. We've seen indie devs hit $4k MRR in 3 months with a tight focus on one sector.
Where the API earns its keep
/companies/ticker/:ticker/latest— single call per ticker to detect fresh transcripts. The new exact-match semantics meanALABreturns Astera Labs only, never accidentally pulling an Indian listing with a substring-matching symbol./transcripts/:id?format=full— full text in one request, no pagination./search/by_ticker— for theme cross-cuts, replaces N requests with 1.