The problem
ICP-fit research tells you who fits. It does not tell you when to act. The buying window is a moving target — a Series B closed last week, a new VP Sales hired yesterday, an open GTM Engineer JD posted four days ago, a public statement that the company is evaluating Clay. Most “AI for sales” tools surface these as scraped noise: undated, unverified, attributed to URLs that may or may not contain what was claimed. The Account Research Agent answered who. This tool answers when.
What it does
Input: a YAML list of company domains. Output: a weekly email digest of dated buying-window signals — every signal verified against its live source URL by code before it ships.
Six typed signal classes, each with its own primary source, recency window, and identity-key contract for dedup:
| Type | Primary source | Recency window | Why it matters |
|---|---|---|---|
funding_round | Exa | 120 days | Cash-in opens a tooling spend window within 60-90 days |
exec_move | Exa | 90 days | New CRO / COO / Head of RevOps has a 90-day mandate to rebuild the stack |
relevant_jd | Tavily | 45 days | They’re admitting they don’t have the capability internally yet |
product_launch | Perplexity | 60 days | Growth pressure triggers outbound + RevOps re-tooling |
tech_signal | Exa | 30 days | They’re touching the GTM stack right now |
news_strategic | Perplexity | 60 days | M&A, IPO file, layoffs, reorg — priorities and tooling realign |
Anything that doesn’t fit one of the six types gets dropped. The taxonomy is the contract.
The architecture is a LangGraph state machine: six producers fan out as a parallel super-step, the critic runs an evidence-traceability check in code before any LLM judgment, the orchestrator selectively re-fires failed producers via Anthropic’s native web_search fallback, the deduper writes survivors to SQLite with per-type recency windows so signals don’t re-surface week-over-week.
Architecture
Yellow nodes mark the code-enforced clamps (evidence_traceable in the critic, identity_key dedup in the deduper). The blue diamond is the orchestrator’s routing gate — Command(goto=Sends) for selective re-fire, or straight through to the deduper when no gaps remain or the retry budget is spent. The green node carries the parallel super-step where all six producers fire concurrently and their writes merge via the merge_signals reducer.
Architectural decisions worth defending
1. Three code-enforced rules, not three prompt-asked-nicely rules. The agent has three commitments that can’t be talked around because they live in Python, not prose:
evidence_traceable— the critic fetches each signal’ssource_url, renders it to text, and substring-matches the verbatimevidence_quote. Hallucinated quotes attributed to real URLs score 0 and get dropped. Same architectural move as the disqualifier clamp in the Account Research Agent.- Spend cap — a
SpendTrackerwraps every API call, charges optimistically, and raisesBudgetExceededbefore the next call would push the run total over the per-run cap. Per-company soft cap stops one bad domain from starving the rest. - Identity-key dedup — each signal type owns its own
identity_key()method (e.g.funding_roundkeys on(company, round_type, year_month)). Same Series B reported across five outlets = one identity. Stored in SQLite, scoped by per-type recency window, so a 6-month-old funding round naturally exits the active dedup set and stops surfacing.
2. Critic-driven selective re-fire via LangGraph Command(goto=Sends). When a signal fails the critic, the orchestrator doesn’t re-run everything. It emits a Send to the specific producer that owns the failed type, with a hint to use the Anthropic native web_search fallback. Other producers stay put. Same Send pattern as the Account Research Agent’s Gap.target_node, applied to source-affinity rather than gap-affinity.
3. Modular, not coupled. The Account Research Agent answers “who fits and why” once per company. Signal Monitor answers “when to act on the fit you already validated” weekly per company. The two compose as a pre-outbound stack — but neither depends on the other. Signal Monitor’s watchlist contract is a flat YAML, not an ARA brief, so forkers can run it standalone.
First live run
Five-company watchlist (Vercel, Retool, Anthropic, Clay, Apollo) on 2026-06-06:
Subject: Signal Monitor — 6 new signals across 3 companies
VERCEL.COM [relevant_jd] Open req: GTM Engineer (US Remote) (confidence: 0.95) Why this matters: Open GTM Engineer req at Vercel = internal gap acknowledged. Position yourself as the ready-now architect before a 9-month hire fills it.
APOLLO.IO [relevant_jd] Open req: GTM Engineer (Remote, US) (confidence: 0.95) Why this matters: Apollo is hiring exactly the role you fill — position yourself as the practitioner they’re trying to replicate internally.
ANTHROPIC.COM [exec_move] Hire: VP Sales (confidence: 0.85) Why this matters: New segment-specific sales leader = active GTM motion build. Hook: position your stack as the infrastructure she needs to ramp fast.
(… 3 more signals …)
Watchlist: 5 companies · Signals this week: 6 · Run cost: $0.56 / $4.00 cap
Every quote in every signal was verified against its live source URL by the evidence_traceable clamp before the digest was sent. Total wall-clock: ~90 seconds.
Eval discipline
Offline replay tests run on every commit and include a deliberate hallucination-bait snapshot — same HTML page as a real funding announcement, but with a fabricated evidence_quote swapping the round letter, dollar amount, and lead investor. The clamp scores it 0 and the critic rejects it. If that test ever goes green by accident, the architectural commitment is broken.
$ pytest evals/replay/ -v
test_evidence_check_snapshot[exec_move_positive.expected.json] PASSED
test_evidence_check_snapshot[funding_round_hallucinated.expected.json] PASSED ← the load-bearing test
test_evidence_check_snapshot[funding_round_positive.expected.json] PASSED
test_evidence_check_snapshot[relevant_jd_positive.expected.json] PASSED
test_evidence_check_snapshot[spa_shell.expected.json] PASSED
5 passed in 0.06s
Part of the pre-outbound stack
This is the second repo in a three-stage agentic pre-outbound system. Each repo runs standalone; together they compose end-to-end.
| Repo | Answers | Output contract |
|---|---|---|
| Account Research Agent | Who fits | JSON via python run.py <domain> --json |
| Signal Monitor (this tool) | When to act | JSON via python run.py --only <domain> --json |
| Meeting Prep Agent | What to say | Markdown + JSON sidecar via python run.py <input.yaml> |
Downstream, Outbound Campaign Crew (CrewAI) extends the stack into outbound generation — claim-traceable sequences gated by ARA’s verdict.
And the discipline layer above all of them:
| Repo | Role |
|---|---|
| eval-watch | How do you know your agents — all of them — are right? Monthly GH Actions cron, regression gate vs anchored baseline, STATUS.md auto-committed to the repo. |
Three MIT repos. Three architecturally-coherent state machines. One thesis: code-enforced rules over prompt-asked-nicely rules, critic-driven self-correction, modular composition through stable CLI contracts. Same pattern shows up seven times across the portfolio: disqualifier clamp (ARA) → length compliance (sister project, the blog autopilot) → three clamps (here) → concurrency clamp in this runtime → four clamps + sequential composition in MPA → eval discipline enforced in CI (eval-watch) → Flow gates + task guardrails in CrewAI (Outbound Campaign Crew).
Use it
The agent is MIT licensed. Bring your own API keys (Anthropic + Exa + Tavily + Perplexity + Resend). Edit watchlist.yaml with the companies you want to watch. Install the launchd autopilot and the digest hits your inbox every Saturday at 7am.
git clone https://github.com/aehirota/signal-monitor
cd signal-monitor
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in API keys
python run.py --dry-run # smoke test — prints digest to stdout
./scripts/load-launchd.sh # weekly autopilot
Architecture deep-dive in docs/architecture.md. Honest limitations in KNOWN_ISSUES.md — the clamp is strict by design, which produces predictable false negatives on JS-rendered pages and anti-bot-protected sites. The trade-off (recoverable false negative over unrecoverable false positive) is documented with real examples from the first live run.