DEV Community

Vhub Systems
Vhub Systems

Posted on

How to Build a Funding Round Alert That Notifies You When a Target Account Raises Money

How to Build a Funding Round Alert That Notifies You When a Target Account Raises Money

A company that just closed a funding round has three things simultaneously: fresh budget, executive mandate to spend it, and urgency to show results. That 30–60 day window after the announcement is the single highest-intent buying window in B2B sales.

The problem: most sales reps find out from a LinkedIn post — days or weeks after the announcement. By then, the initial wave of vendor outreach has already hit the inbox. The window is narrowing.

The Real Cost of Finding Out Late

When a target account closes a Series A or B, they are actively replacing legacy tools, hiring fast, and fielding vendor calls. The average post-funding buying window is 30–60 days. A rep who shows up on day 45 is competing against vendors who showed up on day 2.

Salesforce does not fire an alert when a target account raises money. HubSpot does not either. Crunchbase has the data — but manual checks do not scale across a 200-account target list. No one checks 200 Crunchbase pages every morning.

What Intent Platforms Charge to Solve This

Enterprise intent platforms have built businesses around this exact signal:

  • Bombora: $15,000–$40,000/year — identifies "surging" accounts based on content consumption + event signals
  • 6sense: $60,000+/year — predictive scoring that incorporates funding events, hiring signals, and web behavior
  • ZoomInfo SalesOS: $15,000+/year — includes funding alerts as part of the full enrichment suite

These tools are out of reach for most AEs, SDRs, and founders running lean outbound stacks. But the underlying data — funding announcements — is public.

The Architecture

The approach uses Apify's google-serp-scraper actor to watch search queries like:

"[company name]" "funding" OR "raised" OR "Series A" OR "Series B" site:techcrunch.com OR site:crunchbase.com
Enter fullscreen mode Exit fullscreen mode

For each account in your target list, you run this query on a schedule. If new results appear that weren't in your stored baseline — a new press release, a Crunchbase entry, a TechCrunch announcement — you flag it as a detected funding event.

Components:

  1. Account list loader — reads target accounts from Google Sheets
  2. Apify actor runner — executes google-serp-scraper per company
  3. Delta detector — compares results against stored baseline snapshot
  4. Alert dispatcher — posts to Slack when new funding detected
  5. CRM writer — updates HubSpot/Pipedrive account record

Implementation

Step 1: Load Target Accounts from Google Sheets

const { google } = require("googleapis");

async function loadAccountList(spreadsheetId, range) {
  const auth = new google.auth.GoogleAuth({
    scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
  });
  const sheets = google.sheets({ version: "v4", auth });
  const res = await sheets.spreadsheets.values.get({
    spreadsheetId,
    range,
  });
  return (res.data.values || []).map((row) => ({
    name: row[0],
    domain: row[1] || null,
  }));
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Run Apify SERP Scraper Per Company

const { ApifyClient } = require("apify-client");

const client = new ApifyClient({ token: process.env.APIFY_API_TOKEN });

async function searchFundingSignals(companyName) {
  const query = `"${companyName}" "raised" OR "funding" OR "Series A" OR "Series B" site:techcrunch.com OR site:crunchbase.com`;

  const run = await client.actor("apify/google-search-scraper").call({
    queries: query,
    maxPagesPerQuery: 1,
    resultsPerPage: 10,
    languageCode: "en",
    countryCode: "us",
  });

  const { items } = await client.dataset(run.defaultDatasetId).listItems();
  return items.flatMap((page) => page.organicResults || []).map((r) => ({
    title: r.title,
    url: r.url,
    snippet: r.description || "",
  }));
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Delta Detection Against Stored Baseline

const fs = require("fs");
const path = require("path");

function loadBaseline(companyName) {
  const file = path.join("./baselines", `${companyName.replace(/\s+/g, "_")}.json`);
  if (!fs.existsSync(file)) return [];
  return JSON.parse(fs.readFileSync(file, "utf8"));
}

function saveBaseline(companyName, results) {
  const dir = "./baselines";
  if (!fs.existsSync(dir)) fs.mkdirSync(dir);
  const file = path.join(dir, `${companyName.replace(/\s+/g, "_")}.json`);
  fs.writeFileSync(file, JSON.stringify(results, null, 2));
}

function detectNewFundingResults(current, baseline) {
  const baselineUrls = new Set(baseline.map((r) => r.url));
  return current.filter((r) => !baselineUrls.has(r.url));
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Slack Alert on New Funding Detection

async function sendFundingAlert(companyName, newResults) {
  const blocks = [
    {
      type: "header",
      text: {
        type: "plain_text",
        text: `💰 Funding Signal Detected: ${companyName}`,
      },
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*${newResults.length} new result(s)* found for *${companyName}*.\nThis may indicate a recent funding announcement.`,
      },
    },
    ...newResults.slice(0, 3).map((r) => ({
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*${r.title}*\n${r.snippet.substring(0, 120)}...\n<${r.url}|View source>`,
      },
    })),
  ];

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ blocks }),
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 5: CRM Write-Back (HubSpot)

When a funding signal is detected, update the account in your CRM:

async function updateHubSpotAccount(companyDomain, fundingDetails) {
  // Search for company in HubSpot by domain
  const searchRes = await fetch(
    "https://api.hubapi.com/crm/v3/objects/companies/search",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        filterGroups: [
          {
            filters: [
              {
                propertyName: "domain",
                operator: "EQ",
                value: companyDomain,
              },
            ],
          },
        ],
      }),
    }
  );
  const searchData = await searchRes.json();
  const companyId = searchData.results?.[0]?.id;
  if (!companyId) return;

  // Tag as Recently Funded
  await fetch(
    `https://api.hubapi.com/crm/v3/objects/companies/${companyId}`,
    {
      method: "PATCH",
      headers: {
        Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        properties: {
          recent_funding_signal: "true",
          funding_detected_date: new Date().toISOString().split("T")[0],
        },
      }),
    }
  );

  // Create high-priority outreach task
  await fetch("https://api.hubapi.com/crm/v3/objects/tasks", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      properties: {
        hs_task_subject: `[FUNDING SIGNAL] Reach out to ${companyDomain} — new round detected`,
        hs_task_priority: "HIGH",
        hs_task_status: "NOT_STARTED",
        hs_task_type: "TODO",
        hs_timestamp: Date.now() + 24 * 60 * 60 * 1000,
      },
      associations: [
        {
          to: { id: companyId },
          types: [{ associationCategory: "HUBSPOT_DEFINED", associationTypeId: 192 }],
        },
      ],
    }),
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Main Runner — Wire It All Together

async function runFundingMonitor() {
  const accounts = await loadAccountList(
    process.env.GOOGLE_SHEETS_ID,
    "Accounts!A2:B"
  );

  for (const account of accounts) {
    const current = await searchFundingSignals(account.name);
    const baseline = loadBaseline(account.name);
    const newResults = detectNewFundingResults(current, baseline);

    if (newResults.length > 0) {
      console.log(`New funding signal: ${account.name}${newResults.length} new result(s)`);
      await sendFundingAlert(account.name, newResults);
      if (account.domain) {
        await updateHubSpotAccount(account.domain, newResults);
      }
    }

    // Always update baseline with current results
    saveBaseline(account.name, current);

    // Respect Apify rate limits
    await new Promise((r) => setTimeout(r, 2000));
  }
}

runFundingMonitor().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Step 7: Automate with GitHub Actions

Schedule this to run daily:

name: Funding Monitor

on:
  schedule:
    - cron: "0 7 * * *"  # 7 AM UTC daily
  workflow_dispatch:

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm install
      - run: node monitor.js
        env:
          APIFY_API_TOKEN: ${{ secrets.APIFY_API_TOKEN }}
          GOOGLE_SHEETS_ID: ${{ secrets.GOOGLE_SHEETS_ID }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Cost: $3–$7/Month vs $15,000–$60,000/Year

The cost breakdown for a 100-account target list:

  • Apify google-search-scraper: ~$0.03–0.07 per run × 100 accounts = $3–7/month
  • GitHub Actions: free (within free tier)
  • Google Sheets API: free
  • HubSpot API: free (CRMoperations on existing records)

Total: $3–$7/month.

Compared to Bombora ($15,000–$40,000/year), 6sense ($60,000+/year), or ZoomInfo SalesOS ($15,000+/year), this is a 99%+ cost reduction for the same underlying signal source: public funding announcements indexed by Google.

What You Get

  • Daily scan of your target account list for new funding mentions
  • Slack alert with company name, announcement snippet, and source link
  • HubSpot account automatically tagged as "Recently Funded"
  • High-priority task created for AE to act within the window
  • Baseline delta detection so you only alert on new announcements, not recurring results

The 30–60 day post-funding window is real. The data is public. The gap is the system to catch it automatically — and that system takes an afternoon to build.

Top comments (0)