Qualifying Accounts by Funding Amount with TypeScript
Qualifying Accounts by Funding Amount with TypeScript
Source and qualify companies by their real total funding — not "most recent funding round" (Clay has no funding-date field anywhere, despite one routine's description implying otherwise) — using Clay's Search API and the Company Latest Funding routine, in a small TypeScript/Express service.
What you will build
An Express service exposing POST /qualify-by-funding, which sources a candidate pool by a funding-amount range via Search, then confirms each candidate's real total funding via a routine call, and returns companies whose funding clears a caller-supplied threshold.
POST /qualify-by-funding (fundingAmounts, minAmountUsd, maxCandidates, maxFundingChecks)
↓
POST /search/filters-mode (create, filtered by funding_amounts)
↓
POST /search/filters-mode/{id}/run (page candidate companies)
↓
POST /routines/{routine_id}/run (Company Latest Funding, per domain, capped at 5)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
qualified: companies whose real funding amount >= minAmountUsd
AI Prompt
Implement a TypeScript/Express service that qualifies a TAM by funding
amount using Clay's Public API.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Search filters-mode has a real "funding_amounts" enum field for
companies (values: under_1m, 1m_5m, 5m_10m, 10m_25m, 25m_50m, 50m_100m,
100m_250m, over_250m, unknown). There is NO funding-date/recency field
anywhere in the filter catalog. Do not invent one, and do not claim you
can source by "most recent" funding round -- that fact does not exist
in Clay's Search API.
- The "Company Latest Funding" routine (id via
CLAY_ROUTINE_ID_LATEST_FUNDING env var) takes
{"Company Domain": "<hostname>"}. Its OWN description claims it returns
"stage, amount, date, and investors" -- in reality this is
MISLEADING: the real result contains only a single numeric amount under
the key "Latest Funding" (not the display name "Company Latest
Funding"). Do not build any code path that expects a date, stage, or
investor list from this routine -- it does not exist.
- Cap per-company routine calls at <=5 per request -- each costs real
Clay credits (~6.4 credits/call here, with variable pricing).
- Run the verification step below before finishing.
Prerequisites
- Node.js 18+ and TypeScript
- A Clay Public API key (
clay_scoped_...) - The routine ID for
Company Latest Funding, discovered once viaclay routines list
1. Create the project
mkdir tam-funding-qualify && cd tam-funding-qualify npm init -y npm install express npm install -D typescript @types/express @types/node npx tsc --init
2. Discover the real schema
curl -s "https://api.clay.com/public/v0/search/filters-mode/fields?source_type=companies" \ -H "clay-api-key: $CLAY_API_KEY"
Confirms funding_amounts as a real enum, no date field. Then:
clay routines list | grep -B1 -A3 '"Company Latest Funding"' clay routines get function:t_XXXX
Schema: {"Company Domain": "<hostname>"}, estimatedCreditCost.perRun: 6.4 (variable pricing).
3. Configure credentials
CLAY_API_KEY= CLAY_ROUTINE_ID_LATEST_FUNDING=
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 FUNDING_ROUTINE_ID = process.env.CLAY_ROUTINE_ID_LATEST_FUNDING;
if (!CLAY_API_KEY) throw new Error("CLAY_API_KEY env var is required");
if (!FUNDING_ROUTINE_ID) throw new Error("CLAY_ROUTINE_ID_LATEST_FUNDING env var is required");
interface CompanyRow {
name: string;
domain: 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 || {}),
},
});
}
// Candidate pool via Search filters-mode, filtered on the real "funding_amounts"
// enum. There is no recency/date filter for funding anywhere in this catalog.
async function searchCompaniesByFundingRange(fundingAmounts: string[], limit: number): Promise<CompanyRow[]> {
const createRes = await clayFetch("/search/filters-mode", {
method: "POST",
body: JSON.stringify({ source_type: "companies", filters: { funding_amounts: fundingAmounts } }),
});
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;
}
// Despite the routine's own description promising "stage,
// amount, date, and investors", the real result contains ONLY a raw numeric
// amount under the key "Latest Funding" (not the display name "Company Latest
// Funding"). There is no date, stage, or investor data -- "recency" is not a
// real, checkable fact from this routine.
async function getLatestFundingAmount(domain: string): Promise<number | null> {
const startRes = await clayFetch(`/routines/${FUNDING_ROUTINE_ID}/run`, {
method: "POST",
body: JSON.stringify({ items: [{ id: domain, inputs: { "Company Domain": 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?: Record<string, string> }>;
};
if (body.status === "complete") {
const raw = body.data[0]?.result?.["Latest Funding"];
return raw ? Number(raw) : 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("/qualify-by-funding", async (req: Request, res: ExpressResponse) => {
const { fundingAmounts, minAmountUsd, maxCandidates = 20, maxFundingChecks = 5 } = req.body ?? {};
if (!fundingAmounts || !minAmountUsd) {
return res.status(400).json({ error: "fundingAmounts and minAmountUsd are required" });
}
const checkLimit = Math.min(maxFundingChecks, 5);
try {
const candidates = await searchCompaniesByFundingRange(fundingAmounts, maxCandidates);
const toCheck = candidates.slice(0, checkLimit);
const qualified: Array<{ domain: string; name: string; latestFundingUsd: number }> = [];
const unknown: Array<{ domain: string; name: string }> = [];
for (const company of toCheck) {
const amount = await getLatestFundingAmount(company.domain);
if (amount === null) {
unknown.push({ domain: company.domain, name: company.name });
} else if (amount >= minAmountUsd) {
qualified.push({ domain: company.domain, name: company.name, latestFundingUsd: amount });
}
}
res.json({
candidatesFound: candidates.length,
fundingChecked: toCheck.length,
qualified,
unknown,
});
} 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/qualify-by-funding \
-H "Content-Type: application/json" \
-d '{"fundingAmounts":["over_250m"],"minAmountUsd":500000000,"maxCandidates":20,"maxFundingChecks":2}'
6. Verify the result
{
"candidatesFound": 20,
"fundingChecked": 2,
"qualified": [
{"domain": "apple.com", "name": "Apple", "latestFundingUsd": 4500000000}
],
"unknown": []
}
A real, numeric funding amount ($4.5B) was returned for Apple and correctly qualified against the $500M threshold.
How it works
The funding_amounts search filter and the Company Latest Funding routine answer different, narrower questions than "source by recent funding" implies: Search can only bucket by a coarse pre-defined amount range, and the routine — despite its own description claiming otherwise — only returns a single number, not a date or round stage. So true recency-based prioritization isn't achievable at all from Clay's real API surface today; what's real is qualifying against a funding amount threshold, refreshed per-company via the routine rather than trusted purely from the coarse search bucket.
Common issues
Expecting a funding date, round stage, or investor list from the routine
Cause: trusting the routine's own description text ("returns details on the latest funding round including stage, amount, date, and investors") instead of the actual live response.
Fix: the real result has exactly one field, "Latest Funding" — a JSON string of digits (e.g. "6500000000"), not a JSON number, and nothing else. Always parse it with Number() before comparing; never require or assert it's already numeric. Confirm this yourself with a live call rather than trusting the routine's description.
Reading result["Company Latest Funding"] returns undefined
Cause: reading the routine's display name instead of its actual result key.
Fix: the real key is "Latest Funding".
Next steps
- Combine with
industriesorlocation_regions_includeto narrow the candidate pool before spending routine credits. - Feed qualified accounts into the firmographic lead-scoring example for a composite score.
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/qualify-by-funding -d '{\"fundingAmounts\":[\"over_250m\"],\"minAmountUsd\":500000000,\"maxCandidates\":20,\"maxFundingChecks\":2}'"
expected_result: "A real numeric funding amount is returned and correctly qualified for at least one checked domain, read from the key 'Latest Funding', with no date, stage, or investor data and no funding-recency search filter used anywhere."