Agent recipe
Find contact emails for a list of domains
Go from a bare domain to the right person's work email: confirm the company, find who holds the role and look up the address.
Going from a bare domain to the right person's work email takes three moves: confirm which company the domain belongs to, find who holds the role you want, and look up that person's address. Each move is one capability below.
Email finders return business addresses for people at a company domain. Use them for legitimate outreach and follow the rules that apply to you, such as GDPR and CAN-SPAM. Input fields can differ from one source to the next, so read a tool's schema with GET /v1/tools/{id} before you call it.
Steps
Confirm the company behind each domain
Company enrichment · Company enrichment
Enrich each domain to confirm the company's name, its size and what it does before you look for people.
Example input { "domain": "notion.so" }No tool in the catalog serves this step right now. The rest of the recipe still runs; check back for this one.
Find who holds the role
Web search · Web search
Search the web for the role you want at that company, for example its head of partnerships, and note the person's name.
Example input { "query": "Notion head of partnerships" }No tool in the catalog serves this step right now. The rest of the recipe still runs; check back for this one.
Look up the work email
Find an email · Email finder & verification
Find the business email address for that name at the company's domain.
Example input { "domain": "notion.so", "first_name": "Jane", "last_name": "Doe" }Tools for “Look up the work email” Tool Price Success (7 days) Median latency Runs (30 days) Tier Check a bulk work-email lookup jobReturns the current status of a bulk person email enrichment job — once completed, includes the results_url to download the full results $0.001 / call — — — bridged Find a work email from a name and domainName + company domain → the person's verified work email, with confidence score and the sources it was seen on $0.0216 / call — — — bridged Find an article author's email from its URLArticle or blog-post URL → the byline author's name and work email $0.0216 / call — — — bridged
Run the whole recipe
# Needs curl and jq. Step 3 is shown; the other steps work the same way.
TOOL_ID=$(curl -s https://api.orcarouter.ai/v1/tools/discover \
-H "Authorization: Bearer $ORCAROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q": "email finder", "limit": 1}' | jq -r '.data.results[0].id')
# The inputs the tool takes
curl -s "https://api.orcarouter.ai/v1/tools/$TOOL_ID" \
-H "Authorization: Bearer $ORCAROUTER_API_KEY" | jq '.data.input_schema'
# Run it with those inputs
curl -s "https://api.orcarouter.ai/v1/tools/$TOOL_ID/run" \
-H "Authorization: Bearer $ORCAROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": {"domain": "notion.so", "first_name": "Jane", "last_name": "Doe"}}'# pip install requests openai
import json
import os
import requests
from openai import OpenAI
BASE = "https://api.orcarouter.ai"
KEY = os.environ["ORCAROUTER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}
# One discover query per step of the recipe.
STEPS = ["company enrichment", "web search", "email finder"]
TASK = "Find the head of partnerships at notion.so and their work email address."
def discover(query, limit=2):
r = requests.post(f"{BASE}/v1/tools/discover", json={"q": query, "limit": limit}, headers=HEADERS, timeout=30)
r.raise_for_status()
return [tool["id"] for tool in r.json()["data"]["results"]]
def run(name, tool_input):
r = requests.post(f"{BASE}/v1/tools/{name}/run", json={"input": tool_input}, headers=HEADERS, timeout=120)
return r.text # the tool's answer, or its error, goes back to the model
ids = [tool_id for query in STEPS for tool_id in discover(query)]
fns = requests.get(
f"{BASE}/v1/tools/functions",
params={"ids": ",".join(ids), "format": "openai"},
headers=HEADERS,
timeout=30,
)
fns.raise_for_status()
tools = fns.json()["data"]["tools"]
llm = OpenAI(base_url=f"{BASE}/v1", api_key=KEY)
messages = [{"role": "user", "content": TASK}]
for _ in range(10):
msg = llm.chat.completions.create(model="openai/gpt-5-mini", messages=messages, tools=tools).choices[0].message
messages.append(msg)
if not msg.tool_calls:
print(msg.content)
break
for call in msg.tool_calls:
result = run(call.function.name, json.loads(call.function.arguments or "{}"))
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})// npm install openai
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
const BASE = "https://api.orcarouter.ai";
const KEY = process.env.ORCAROUTER_API_KEY!;
const headers = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
// One discover query per step of the recipe.
const STEPS = ["company enrichment", "web search", "email finder"];
const TASK = "Find the head of partnerships at notion.so and their work email address.";
async function discover(q: string, limit = 2): Promise<string[]> {
const res = await fetch(`${BASE}/v1/tools/discover`, {
method: "POST",
headers,
body: JSON.stringify({ q, limit }),
});
if (!res.ok) throw new Error(`discover: HTTP ${res.status}`);
const { data } = await res.json();
return data.results.map((tool: { id: string }) => tool.id);
}
async function run(name: string, input: unknown): Promise<string> {
const res = await fetch(`${BASE}/v1/tools/${name}/run`, {
method: "POST",
headers,
body: JSON.stringify({ input }),
});
return res.text(); // the tool's answer, or its error, goes back to the model
}
async function main() {
const ids = (await Promise.all(STEPS.map((q) => discover(q)))).flat();
const fns = await fetch(
`${BASE}/v1/tools/functions?format=openai&ids=${encodeURIComponent(ids.join(","))}`,
{ headers },
);
if (!fns.ok) throw new Error(`functions: HTTP ${fns.status}`);
const { data } = await fns.json();
const llm = new OpenAI({ baseURL: `${BASE}/v1`, apiKey: KEY });
const messages: ChatCompletionMessageParam[] = [{ role: "user", content: TASK }];
for (let turn = 0; turn < 10; turn++) {
const reply = await llm.chat.completions.create({ model: "openai/gpt-5-mini", messages, tools: data.tools });
const msg = reply.choices[0].message;
messages.push(msg);
if (!msg.tool_calls?.length) {
console.log(msg.content);
return;
}
for (const call of msg.tool_calls) {
const fn = call.type === "function" ? call.function : undefined;
const result = fn ? await run(fn.name, JSON.parse(fn.arguments || "{}")) : "unsupported tool call";
messages.push({ role: "tool", tool_call_id: call.id, content: result });
}
}
}
main();More recipes
- Monitor brand mentions on the web and TikTokSearch the web and TikTok for your brand, then read the comments under the posts that mention it: a daily brand check an agent can run on its own.
- Enrich a lead listTurn a list of company domains into firmographics, a contact's work email and a recent news hook, with one API key.
- Brief yourself on a DeFi protocolPull a protocol's total value locked and its latest news, so an agent can write a one-page brief with figures and sources.
