Scoring and Ranking a Sourced TAM with TypeScript
Scoring and Ranking a Sourced TAM with TypeScript
Rank a sourced list of companies by a weighted ICP-fit score — computed entirely in your own code from real Clay data, since Clay has no native scoring or ranking endpoint — using Search plus Enrich Company, in a small TypeScript/Express service.
What you will build
An Express service exposing POST /score-tam, which sources a candidate pool by industry, enriches each candidate with real firmographic data, computes a custom weighted score, and returns the pool ranked highest-to-lowest.
POST /score-tam (industry, maxCandidates, maxScored)
↓
POST /search/filters-mode (create, filtered by industries)
↓
POST /search/filters-mode/{id}/run (page candidate companies)
↓
POST /routines/{routine_id}/run (Enrich Company, per domain, capped at 5)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
score in app code (employee_count + annual_revenue) → ranked list
AI Prompt
Implement a TypeScript/Express service that scores and ranks a sourced TAM using Clay's Public API for the raw data and your own weighting logic for the score. Requirements: - Base URL: https://api.clay.com/public/v0, auth header "clay-api-key". - Clay has NO native ICP-fit scoring or ranking endpoint, and no "Claygent" REST endpoint either -- Claygent is a separate workflow-builder feature, not part of this Public API. Do not invent a scoring endpoint; compute the score entirely in your own code. - Source candidates via Search filters-mode on the real "industries" enum field (one of 30 real fields for companies, source_type companies). - Enrich each candidate via the "Enrich Company" routine (id via CLAY_ROUTINE_ID_ENRICH_COMPANY env var), which returns real employee_count (a number) and annual_revenue (a band string like "1B-10B") under the result key "Enrich Company". - Compute a weighted score from those two real fields with clearly documented, illustrative weights -- make explicit in comments that this weighting is an example, not something Clay provides or endorses. - Cap enrichment calls at <=5 per request -- each costs real Clay credits (1 credit/call here). - 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-score-rank && cd tam-score-rank npm init -y npm install express npm install -D typescript @types/express @types/node npx tsc --init
2. Discover the real schema
Reuses the industries search field and the Enrich Company routine's real output shape (employee_count, annual_revenue) from the TAM sourcing and waterfall-enrichment examples.
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");
interface CompanyRow {
name: string;
domain: string;
industry: string;
}
interface EnrichedCompany {
name?: string;
domain?: string;
employee_count?: number;
annual_revenue?: string;
}
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 || {}),
},
});
}
async function searchCompaniesByIndustry(industry: string, limit: number): Promise<CompanyRow[]> {
const createRes = await clayFetch("/search/filters-mode", {
method: "POST",
body: JSON.stringify({ source_type: "companies", filters: { industries: [industry] } }),
});
if (!createRes.ok) {
throw new Error(`search create failed: ${createRes.status} ${await createRes.text()}`);
}
const { search_id } = (await createRes.json()) as { search_id: string };
const runRes = await clayFetch(`/search/filters-mode/${search_id}/run`, {
method: "POST",
body: JSON.stringify({ limit }),
});
if (!runRes.ok) {
throw new Error(`search run failed: ${runRes.status} ${await runRes.text()}`);
}
const { data } = (await runRes.json()) as { data: CompanyRow[] };
return data;
}
async function enrichCompany(domain: string): Promise<EnrichedCompany | null> {
const startRes = await clayFetch(`/routines/${ENRICH_COMPANY_ROUTINE_ID}/run`, {
method: "POST",
body: JSON.stringify({ items: [{ id: domain, inputs: { "Company Identifier": domain } }] }),
});
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`);
}
// This weighting is entirely custom app logic -- Clay has no native "ICP fit
// score" or ranking endpoint (no such field/endpoint exists, and
// Claygent, which might sound relevant, is a separate workflow-builder
// feature not reachable via this REST API at all). These weights are an
// illustrative example, not something Clay computes or endorses.
const REVENUE_BAND_SCORE: Record<string, number> = {
"0-500K": 1, "500K-1M": 2, "1M-5M": 3, "5M-10M": 4, "10M-25M": 5,
"25M-75M": 6, "75M-200M": 7, "200M-500M": 8, "500M-1B": 9,
"1B-10B": 10, "10B-100B": 10, "100B-1T": 10,
};
function scoreCompany(enriched: EnrichedCompany): number {
const employeeScore = Math.min((enriched.employee_count ?? 0) / 100, 10);
const revenueScore = REVENUE_BAND_SCORE[enriched.annual_revenue ?? ""] ?? 0;
return Math.round((employeeScore * 0.4 + revenueScore * 0.6) * 10) / 10;
}
const app = express();
app.use(express.json());
app.post("/score-tam", async (req: Request, res: ExpressResponse) => {
const { industry, maxCandidates = 20, maxScored = 5 } = req.body ?? {};
if (!industry) {
return res.status(400).json({ error: "industry is required" });
}
const scoreLimit = Math.min(maxScored, 5);
try {
const candidates = await searchCompaniesByIndustry(industry, maxCandidates);
const toScore = candidates.slice(0, scoreLimit);
const scored: Array<{ name: string; domain: string; score: number }> = [];
for (const company of toScore) {
const enriched = await enrichCompany(company.domain);
if (enriched) {
scored.push({ name: company.name, domain: company.domain, score: scoreCompany(enriched) });
}
}
scored.sort((a, b) => b.score - a.score);
res.json({ candidatesFound: candidates.length, scored: scored.length, ranked: scored });
} 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/score-tam \
-H "Content-Type: application/json" \
-d '{"industry":"Software Development","maxCandidates":20,"maxScored":2}'
6. Verify the result
{
"candidatesFound": 20,
"scored": 2,
"ranked": [
{"name": "Google", "domain": "google.com", "score": 10},
{"name": "Amazon", "domain": "amazon.com", "score": 10}
]
}
Both real companies were enriched with real data and ranked by the custom score.
How it works
There's no Clay endpoint that produces an "ICP fit score" — that phrase describes a business concept the API doesn't model at all. Real ICP scoring is always a composition: Search narrows the candidate pool on a real filter, Enrich Company supplies real per-company data, and the score itself is arithmetic you write and own. The specific weights above (40% employee count, 60% revenue band) are illustrative — a real ICP model should reflect your own qualification criteria, not this example's numbers.
Common issues
Looking for a Claygent-based or native scoring endpoint
Cause: the content plan's own language ("ICP fit") and Clay's own "Claygent" branding suggest there might be a built-in scoring agent.
Fix: Claygent is a workflow-builder feature (clay workflows / agent nodes), not part of the Public REST API this example uses, and there is no scoring endpoint of any kind. Compute the score yourself.
Treating enrichment cost as free because search is
Cause: search has an effectively unlimited quota, which can make it easy to forget the routine call per candidate is a separate, metered cost.
Fix: cap maxScored explicitly — each Enrich Company call is 1 real credit, small but real per candidate.
Next steps
- Replace the illustrative weights with your own real ICP criteria (e.g. tech stack match, funding recency where available).
- Feed the top-ranked accounts into the contact-waterfall or phone-discovery examples for people-level follow-up.
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/score-tam -d '{\"industry\":\"Software Development\",\"maxCandidates\":20,\"maxScored\":2}'"
expected_result: "Two real companies are sourced, enriched with real employee_count/annual_revenue data, scored by the custom formula, and returned ranked."