TL;DR
- Product updates are public. Poll each target account's changelog or blog feed, flag new entries that match launch words, and you have the trigger.
- LeadOcean does not detect launches. It answers the next question: who at that company should get the email. One search by
domainreturns them. - Setup takes about 45 minutes. Watching costs nothing. Each launch you act on spends roughly 5 to 10 LeadOcean records, so the 1,000 free records cover 100 or more launches.
Prerequisites
- A LeadOcean API key in
$LEADOCEAN_API_KEY(the free plan works: 1,000 records, no card). - Python 3.9 or newer. The script uses only the standard library.
- A list of 20 to 100 target accounts with their domains.
- Somewhere to send the result: a Slack webhook, a file or your sequencer's import.
Step 1: List accounts and where they publish
A launch shows up in one of three places: a changelog page, a blog or an RSS/Atom feed. Feeds are the cheapest to poll. Start with the ones that have a feed and add the rest later.
Save this as watchlist.json. The accounts below are placeholders.
[
{"domain": "acme.com", "feed": "https://acme.com/changelog/feed.xml"},
{"domain": "example.io", "feed": "https://example.io/blog/rss.xml"}
]Check a feed exists before you add it. Open the page, view the source and look for a <link rel="alternate" type="application/rss+xml"> tag. Many changelog tools expose /feed.xml or /rss, but confirm each one.
Step 2: Poll the feeds and keep new launch entries
Run a script that fetches each feed, remembers what it has seen and prints only new entries with launch words in the title. State lives in a local file.
import json, re, urllib.request, xml.etree.ElementTree as ET
WORDS = re.compile(r"\b(launch|introducing|now available|new feature|released?|announcing|v\d+)\b", re.I)
seen = set(json.load(open("seen.json"))) if __import__("os").path.exists("seen.json") else set()
hits = []
for acct in json.load(open("watchlist.json")):
req = urllib.request.Request(acct["feed"], headers={"User-Agent": "launch-watch/1.0"})
root = ET.fromstring(urllib.request.urlopen(req, timeout=20).read())
for item in root.iter():
tag = item.tag.split("}")[-1]
if tag not in ("item", "entry"):
continue
get = lambda n: next((c for c in item if c.tag.split("}")[-1] == n), None)
title = (get("title").text or "").strip() if get("title") is not None else ""
link_el = get("link")
link = (link_el.text or link_el.get("href") or "").strip() if link_el is not None else ""
if link and link not in seen and WORDS.search(title):
hits.append({"domain": acct["domain"], "title": title, "url": link})
seen.add(link)
json.dump(sorted(seen), open("seen.json", "w"))
json.dump(hits, open("hits.json", "w"), indent=2)
print(f"{len(hits)} new launch entries")The first run marks every existing entry as seen but also reports old launch posts. Delete hits.json after run one, or run once with output discarded to seed seen.json.
Keep the word list tight. A loose list buries you in patch notes. If a team ships weekly, filter on introducing and launch only.
Step 3: Size the buyers for free
Before you spend a record, check how many people at that domain fit your target. The count call is free. Send count=true as a query parameter, not in the body.
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": ["acme.com"],
"seniority": ["vp", "head", "director"],
"limit": 1
}'Read meta.total. If it is 0, skip the account. The total is capped at 100,000, which does not matter at one company.
Step 4: Pull the people who own the launch topic
Match the title to what shipped. A new API means engineering and product leads. A new reporting feature means ops or the department that reads reports. Filters are arrays and the body is flat JSON.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": ["acme.com"],
"title": ["Product", "Engineering"],
"seniority": ["vp", "head", "director"],
"emailStatus": ["verified", "catch_all_valid"],
"limit": 5
}'This spends up to 5 records, one per person returned. Keep limit small. Five names per launch is enough for a two-person approach.
Enum values are exact strings. If a call returns nothing, check the allowed values with GET /v1/enums (free).
Step 5: Get the email and write the opener
Search results hold the profile. To get the address, enrich by the row's meta.source_ids.person_id with reveal_email set. Enrich takes exactly one of linkedin_url, email, phone or person_id.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"person_id": "1234567", "reveal_email": true}'Then build the opener from the hit, not from the person. The launch title is the reason for the email.
Subject: Saw {launch title}
Hi {first_name},
Saw {company} shipped {launch title} ({launch url}). Teams that add
this usually hit {the problem you solve} within a quarter.
Worth a 10-minute look?Read email_status before sending. A bounced address is not refunded.
Step 6: Run it on a schedule
Chain steps 2 to 5 in one script and run it daily. Any scheduler works: cron, GitHub Actions or n8n.
0 8 * * 1-5 cd /opt/launch-watch && python3 watch.py && python3 lookup.pyIf you want an agent to do steps 3 to 5, connect the hosted MCP server at https://api.leadocean.io/mcp. It exposes 13 tools, including free leadocean_count_leads for the sizing step. See track job openings for the same pattern on hiring signals.
Which updates are worth a message
Not every release is a reason to write. Rank hits before they reach a person.
- Launches of a new product or module. Highest value. The team has a new budget line and new problems.
- Integrations and API releases. Good for tools that sell to developers or ops. The reader is technical, so link the changelog entry.
- Pricing and plan changes. Strong if you sell a replacement or a cheaper option. Write the same day.
- Bug fixes and patch notes. Skip them. The word filter in step 2 drops most of them.
A simple rule keeps volume sane: send at most one launch-based email per account per 30 days. Log the date in seen.json next to the link so the script can enforce it.
Common mistakes
Three things break this workflow in practice.
- Watching too many accounts at once. Start with 20 and tune the word list before you scale to 100.
- Searching the whole company. A launch about developer tooling should not reach the finance team. Match the
titlefilter to the launch. - Writing about the launch in general terms. The opener must name the exact feature, or it reads like a template.
What you get
The watcher prints one line and writes hits.json. A run looks like this (placeholder data).
[
{
"domain": "acme.com",
"title": "Introducing Acme Workflows",
"url": "https://acme.com/changelog/workflows"
}
]The people search returns the documented shape, abbreviated below (response shape from the API reference, placeholder values).
{
"success": true,
"data": [
{
"profile_data": {"profile_full_name": "Jane Doe", "profile_headline": "VP Product at Acme"},
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_current_experiences": [{"job_title": "VP Product", "company_name": "Acme", "company_domain": "acme.com"}]
},
"meta": {"source_ids": {"person_id": "1234567"}}
}
],
"meta": {"count": 1, "limit": 5, "nextCursor": null}
}The person_id for step 5 sits at meta.source_ids.person_id on each row. Use fields to keep only what you need, for example fields: ["profile_data.profile_full_name", "meta.source_ids"].
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported in the shell that runs the script. |
| 429 | Requests above the plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After and add a short sleep between accounts. |
| 402 | Free records are spent | Upgrade to Solo ($97 a month) or Agency ($497 a month). See pricing. |
Empty data | An enum value that does not match exactly, or no one at that domain fits | Run the free count first and check values with GET /v1/enums. |
| Feed parse error | The URL returns HTML, not XML | Open the URL in a browser and find the real feed link in the page source. |
| Hits flood after the first run | seen.json was empty | Run once to seed state and discard that output. |
FAQ
Does LeadOcean detect product launches itself?
No. LeadOcean holds people and company data. The launch signal comes from the feeds you watch. LeadOcean tells you who to contact once a launch fires.
What if a target has no feed?
Check for a changelog page and poll its HTML for new headings, or watch the company's release notes on its docs site. If neither exists, drop the account to a monthly manual check.
How many records does this spend a month?
It depends on how many launches you act on. At 5 records a launch and 20 launches a month, that is about 100 records. The Free plan holds 1,000 once, so it covers a trial of the whole workflow. Paid plans use a fair-use pace, not credits.
Can I use social posts instead of feeds?
Yes, and some vendors sell that as a keyword alert feed on public posts, for example Crustdata's write-up. The recipe above is self-run and free. A feed service saves the scripting.
Where does this fit with warm outbound?
A launch is a reason to write, which is what makes outbound warm. It sits inside the wider outbound lead generation process as the trigger step.
Find the buyer behind every launch
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →