Agent recipe

Enrich a lead list

Turn a list of company domains into firmographics, a contact's work email and a recent news hook, with one API key.

A list of company domains is a start; a sales team also needs firmographics, a named contact and a reason to reach out. This recipe adds all three: company enrichment for size and industry, an email finder for the contact, and web search for a recent news hook.

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

  1. Add firmographics for each domain

    Company enrichment · Company enrichment

    Look up each company by its domain to fill in its industry, size, location and links.

    Example input
    {
      "domain": "stripe.com"
    }

    No tool in the catalog serves this step right now. The rest of the recipe still runs; check back for this one.

  2. Find a contact's work email

    Find an email · Email finder & verification · 15 tools · from $0.001

    For the person you want to reach, look up a business email address at the company's domain.

    Example input
    {
      "domain": "stripe.com",
      "first_name": "Jane",
      "last_name": "Doe"
    }
    Tools for “Find a contact's work email”
    ToolPriceSuccess (7 days)Median latencyRuns (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

    See all 15 tools for this step

  3. Add a recent news hook

    Web search · Web search

    Search the web for the company's latest news, so the first message can mention something current.

    Example input
    {
      "query": "Stripe news this month"
    }

    No tool in the catalog serves this step right now. The rest of the recipe still runs; check back for this one.

Run the whole recipe

curl
# Needs curl and jq. Step 1 is shown; repeat it with each step's query.
TOOL_ID=$(curl -s https://api.orcarouter.ai/v1/tools/discover \
  -H "Authorization: Bearer $ORCAROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "company enrichment", "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": "stripe.com"}}'
Python
# 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", "email finder", "web search"]
TASK = (
    "For stripe.com and notion.so, give me each company's industry and size, "
    "the work email of its head of marketing if you can find one, "
    "and one recent news headline."
)


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})
TypeScript
// 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", "email finder", "web search"];
const TASK =
  "For stripe.com and notion.so, give me each company's industry and size, " +
  "the work email of its head of marketing if you can find one, " +
  "and one recent news headline.";

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();
© 2026 OrcaRouter

For Providers

Run an inference platform? Get your models on OrcaRouter.

providers@orcarouter.ai

Join our community

Discordsupport@orcarouter.aiXLinkedInGitHubYouTube