Multi-Provider Waterfall Enrichment for a TAM List with TypeScript
Multi-Provider Waterfall Enrichment for a TAM List with TypeScript
Maximize the match rate on a list of company identifiers — using Clay's Enrich Company function, which cascades across data providers internally for each identifier. (There is no provider-configuration input; "waterfall" describes what Clay does internally, not something the caller sets up.)
What you will build
An Express service exposing POST /enrich-tam, which enriches a batch of company identifiers (domain or LinkedIn URL) and reports which matched versus which didn't.
POST /enrich-tam (identifiers[], maxEnrichments)
↓
POST /routines/{routine_id}/run (Enrich Company, per identifier, capped at 5)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
{ requested, matched[], unmatched[], matchRate }
AI Prompt
Implement a TypeScript/Express service that enriches a list of company
identifiers with waterfall enrichment using Clay's Public API.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- The "Enrich Company" routine (id via CLAY_ROUTINE_ID_ENRICH_COMPANY env
var) takes a single field {"Company Identifier": "<domain, LinkedIn
URL, or Sales Navigator ID>"}. There is NO provider-configuration
field and no separate /providers endpoint -- do not invent one. The
"multi-provider waterfall" is internal to Clay; the caller supplies
one identifier per call, nothing more.
- The result key is "Enrich Company" (matches the
routine's display name exactly here -- unlike some other routines,
this one is not a display-name trap, but check per routine rather than
assuming) and contains ~25 real firmographic fields (name, domain,
size, industry, employee_count, annual_revenue, etc.).
- Cap enrichment calls at <=5 per request -- each costs real Clay
credits (1 credit/call here, cheap but still real).
- Run the verification step below before finishing.
Prerequisites
- Node.js 18+ and TypeScript
- A Clay Public API key (
clay_scoped_...) - The routine ID for
Enrich Company, discovered once viaclay routines list
1. Create the project
mkdir tam-waterfall-enrich && cd tam-waterfall-enrich npm init -y npm install express npm install -D typescript @types/express @types/node npx tsc --init
2. Discover the real schema
clay routines list | grep -B1 -A3 '"Enrich Company"' clay routines get function:t_XXXX
Schema: {"Company Identifier": "<hostname>"}, estimatedCreditCost.perRun: 1.
3. Configure credentials
CLAY_API_KEY= CLAY_ROUTINE_ID_ENRICH_COMPANY=
In a CI/sandbox test environment, these are typically provided as pre-configured secrets; in your own deployment, set them as real environment variables or via your platform's secrets manager.
4. Implement the service
import express, { Request, Response as ExpressResponse } from "express";
const CLAY_API_BASE = "https://api.clay.com/public/v0";
const CLAY_API_KEY = process.env.CLAY_API_KEY;
const ENRICH_COMPANY_ROUTINE_ID = process.env.CLAY_ROUTINE_ID_ENRICH_COMPANY;
if (!CLAY_API_KEY) throw new Error("CLAY_API_KEY env var is required");
if (!ENRICH_COMPANY_ROUTINE_ID) throw new Error("CLAY_ROUTINE_ID_ENRICH_COMPANY env var is required");
async function clayFetch(path: string, init: RequestInit = {}): Promise<globalThis.Response> {
return fetch(`${CLAY_API_BASE}${path}`, {
...init,
headers: {
"clay-api-key": CLAY_API_KEY as string,
"Content-Type": "application/json",
...(init.headers || {}),
},
});
}
interface EnrichedCompany {
name?: string;
domain?: string;
industry?: string;
employee_count?: string;
annual_revenue?: string;
country?: string;
}
// "Waterfall" here describes Clay's internal cascade across data providers for
// a single identifier -- there is NO provider-configuration input on this
// routine (a single field, "Company Identifier"). Maximizing match
// rate across a TAM list means calling this once per identifier and tracking
// which ones resolved, not configuring which providers get tried.
async function enrichCompany(identifier: string): Promise<EnrichedCompany | null> {
const startRes = await clayFetch(`/routines/${ENRICH_COMPANY_ROUTINE_ID}/run`, {
method: "POST",
body: JSON.stringify({ items: [{ id: identifier, inputs: { "Company Identifier": identifier } }] }),
});
if (!startRes.ok) {
throw new Error(`routine start failed: ${startRes.status} ${await startRes.text()}`);
}
const { routine_run_id } = (await startRes.json()) as { routine_run_id: string };
const resultsPath = `/routines/run/${routine_run_id}/results`;
for (let attempt = 0; attempt < 10; attempt++) {
const res = await clayFetch(resultsPath);
if (!res.ok) throw new Error(`routine results failed: ${res.status} ${await res.text()}`);
const body = (await res.json()) as {
status: string;
data: Array<{ id: string; status: string; result?: { "Enrich Company"?: EnrichedCompany } }>;
};
if (body.status === "complete") {
return body.data[0]?.result?.["Enrich Company"] ?? null;
}
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error(`routine run ${routine_run_id} did not complete in time`);
}
const app = express();
app.use(express.json());
app.post("/enrich-tam", async (req: Request, res: ExpressResponse) => {
const { identifiers, maxEnrichments = 5 } = req.body ?? {};
if (!Array.isArray(identifiers) || identifiers.length === 0) {
return res.status(400).json({ error: "identifiers must be a non-empty array" });
}
const toEnrich = identifiers.slice(0, Math.min(maxEnrichments, 5));
try {
const matched: Array<{ identifier: string; company: EnrichedCompany }> = [];
const unmatched: string[] = [];
for (const identifier of toEnrich) {
const company = await enrichCompany(identifier);
if (company && company.name) {
matched.push({ identifier, company });
} else {
unmatched.push(identifier);
}
}
res.json({
requested: toEnrich.length,
matched,
unmatched,
matchRate: toEnrich.length ? matched.length / toEnrich.length : 0,
});
} catch (err) {
res.status(502).json({ error: (err as Error).message });
}
});
const port = Number(process.env.PORT) || 3000;
app.listen(port, () => console.log(`listening on ${port}`));
5. Run
npx tsc node --env-file=.env dist/index.js
curl -X POST http://localhost:3000/enrich-tam \
-H "Content-Type: application/json" \
-d '{"identifiers":["notion.so","linear.app"],"maxEnrichments":2}'
6. Verify the result
{
"requested": 2,
"matched": [
{"identifier": "notion.so", "company": {"name": "Notion", "domain": "notion.com", "industry": "Software Development", "size": "501-1,000 employees", "country": "US"}},
{"identifier": "linear.app", "company": {"name": "Linear", "...": "..."}}
],
"unmatched": [],
"matchRate": 1
}
Both real companies matched on the first call.
How it works
"Multi-provider waterfall" is an accurate description of what happens inside Clay when this routine runs, not a knob the API exposes. The caller's job is simply to supply the best identifier available (a LinkedIn URL resolves most reliably per the routine's own schema description) and track which identifiers in a batch came back with real data versus not — that match/no-match outcome per identifier is the actual observable "waterfall result" from the API's point of view.
Common issues
Looking for a way to configure or exclude specific data providers
Cause: assuming "multi-provider waterfall" implies a provider list the caller controls, since that's how the term is used in some other GTM tools.
Fix: no such control exists on this routine — it's a single opaque call per identifier; provider selection is entirely internal to Clay.
Treating an empty/partial result as an error
Cause: expecting every identifier to resolve.
Fix: track unmatched identifiers explicitly (missing name in the result) rather than throwing — a real TAM list will have some non-matches, and that's the actual "match rate" signal this pattern is meant to surface.
Next steps
- Feed matched company domains into the technographic filtering or funding-qualification examples for further enrichment.
- For larger lists, use
/routines/{routine_id}/run-batch/startinstead of looping single calls.
verification:
status: verified
tested_at: "2026-09-02"
product_version: "public/v0"
command: "npx tsc && node --env-file=.env dist/index.js && curl -X POST http://localhost:3000/enrich-tam -d '{\"identifiers\":[\"notion.so\",\"linear.app\"],\"maxEnrichments\":2}'"
expected_result: "Both real company identifiers resolve to real firmographic data (name, domain, industry, size), read from the 'Enrich Company' result key."