Signal Pipeline

A worked example: run detect_signal over a target account list, weight the fired signals, and emit the top-N accounts as a daily Slack digest.

What this example does

  1. Reads a target account list (domains).
  2. Runs detect_signal for each (free dispatch + 5 tokens per fired detector).
  3. Scores results using a weighted rubric.
  4. Sorts and prints the top accounts.

Cost: ~15 tokens per account, so 100 accounts ≈ 1,500 tokens ($15).

Code

1const KEY = process.env.GTM_TOOLS_API_KEY!;
2
3const WEIGHTS: Record<string, number> = {
4 signal_hiring_sales_rep_repost: 5,
5 signal_trustpilot_negative_support_reviews: 4,
6 signal_hiring_support: 3,
7 signal_hiring_sales_leadership: 3,
8 signal_socials_spike: 2,
9 signal_technologies_identified: 2,
10 signal_hiring_sales_rep: 2,
11 signal_hiring_role: 1,
12 signal_trustpilot_negative_reviews: 1,
13};
14
15interface Detection {
16 name: string;
17 fired: boolean;
18 evidence?: unknown[];
19}
20
21interface DetectResponse {
22 domain: string;
23 signals: Detection[];
24}
25
26interface Scored {
27 domain: string;
28 score: number;
29 fired: string[];
30}
31
32async function detect(domain: string, techs: string[]): Promise<DetectResponse | null> {
33 const res = await fetch("https://api.gtm-tools.sh/api/v0/detect_signal", {
34 method: "POST",
35 headers: {
36 "Authorization": `Bearer ${KEY}`,
37 "Content-Type": "application/json",
38 },
39 body: JSON.stringify({ domain, techs }),
40 });
41 if (!res.ok) {
42 console.error(`detect_signal ${domain}: ${res.status}`);
43 return null;
44 }
45 return res.json();
46}
47
48function score(d: DetectResponse): Scored {
49 const fired = d.signals.filter((s) => s.fired).map((s) => s.name);
50 const total = fired.reduce((acc, name) => acc + (WEIGHTS[name] ?? 1), 0);
51 return { domain: d.domain, score: total, fired };
52}
53
54async function run(domains: string[], techs: string[]) {
55 const results: Scored[] = [];
56 for (let i = 0; i < domains.length; i += 5) {
57 const slice = domains.slice(i, i + 5);
58 const detected = await Promise.all(slice.map((d) => detect(d, techs)));
59 for (const d of detected) {
60 if (d) results.push(score(d));
61 }
62 console.log(`scored ${results.length}/${domains.length}`);
63 }
64 return results.sort((a, b) => b.score - a.score);
65}
66
67const TARGETS = [
68 "gymshark.com",
69 "mammaly.de",
70 "kettleandfire.com",
71 "spanx.com",
72 "siena.cx",
73];
74
75const ranked = await run(TARGETS, ["zendesk.com", "intercom.com"]);
76
77console.log("\n=== Top targets ===");
78for (const r of ranked.slice(0, 10)) {
79 if (r.score === 0) break;
80 console.log(`${r.score.toString().padStart(3)} ${r.domain} [${r.fired.join(", ")}]`);
81}

Sample output

scored 5/5
=== Top targets ===
9 gymshark.com [signal_hiring_sales_rep_repost, signal_trustpilot_negative_support_reviews]
4 mammaly.de [signal_trustpilot_negative_support_reviews]
3 kettleandfire.com [signal_hiring_support]
2 spanx.com [signal_socials_spike]

Tuning the rubric

The weights are arbitrary — calibrate them to your motion:

  • Churn-prevention pitch: Weight signal_trustpilot_negative_support_reviews highest.
  • Greenfield SaaS sales: Weight signal_hiring_sales_rep_repost and signal_hiring_sales_leadership highest.
  • DTC marketing tooling: Weight signal_socials_spike highest.

Slack digest

Pipe the ranked list into a Slack webhook for daily review:

1async function postDigest(ranked: Scored[]) {
2 const top = ranked.filter((r) => r.score > 0).slice(0, 10);
3 const text = top
4 .map((r) => `*${r.score}* — \`${r.domain}\` — ${r.fired.join(", ")}`)
5 .join("\n");
6
7 await fetch(process.env.SLACK_WEBHOOK_URL!, {
8 method: "POST",
9 headers: { "Content-Type": "application/json" },
10 body: JSON.stringify({ text: `*Top GTM signals — ${new Date().toISOString().slice(0, 10)}*\n${text}` }),
11 });
12}

Token budget

StepCost
detect_signal × 100 (median 3 detectors fire)~1,500 tokens
Slack post0
Daily total$15

That’s $450/month to keep a rolling signal feed over 100 accounts.

Next steps