People Signals Monitoring
The push half of People Signals. Monitoring watches a cohort of people you already care about - past champions, past contacts, and target-account decision-makers - and fires a warm lead the moment one of them changes jobs, is promoted, or has a qualifying event.
Why push
tl;drPull discovery answers "who should I contact now?" on demand. But the highest-value people event - a known buyer changing jobs - happens on its own schedule, and the window is short. Monitoring is push: it watches the people an agency already has a relationship with and routes a warm lead the moment something changes, instead of waiting for the next search. B2B contact data decays ~2-3% a month; a watch loop catches the transition, not the stale state.
The flagship play: when a past buyer moves to a new company, they bring trust with them. Deals with a prior champion close at far higher win rates and larger sizes, so a job-change alert on a known contact is a warm-intro generator - the single most valuable people signal an agency can act on. Everything else in the watch loop is built around making that one alert reliable and fast.
System shape
two planesTwo planes, mirroring company signal monitoring. A data plane of collectors that re-check watched people and write append-only events; a match plane of per-agency watchlist rules that decide which events become warm leads, and where they go.
Collectors → person_signal_events
Scheduled jobs re-crawl each watched person (LinkedIn URL diff, news, content feeds), pass candidate events through the substantiation gate, and append immutable rows. The graph stores facts; the collector watches them change.
Per-agency watchlist rules
Each agency declares which cohorts and signal kinds it cares about. Every new event is matched against those rules to a 0-1 score, gated by a threshold, and the survivors fan out to the inbox.
System architecture
C4 · containersThe same two planes as concrete, deployable containers - one colour per repo. The data path runs top to bottom; each edge is labeled with what crosses it and how. Solid edges are request-path / synchronous; dashed edges are scheduled or async. Hover any container for what it owns, or trace one event - a champion's job change - down the happy path.
ac-monitor re-crawls watched people, ac-backend owns the person tables and the intel_* write-back, the Claude managed agent scores on Anthropic infra, ac-python-api runs the enrichment fan-out + relay + delivery, ac-frontend renders the inbox. The middle tiers are the heart: on a survivor the agent completes the pair (enrich_person → resolve the new company → enrich_company), each resolved fact is written back to intel_* with provenance, and the fills stream to the open tab as agent.progress / agent.done frames - see §03. This is the mirror of company monitoring: a company signal enters knowing the company and finds the contact; a people signal enters knowing the person and resolves the company. Either way the warm lead is one (company, person) pair. Stream contract
two SSE legs Two SSE legs meet at the ac-python-api relay. Upstream we subscribe to the managed agent's hosted session stream; downstream we emit our own SSE to the browser - the same frame contract company monitoring already speaks. The relay is the only place the two touch.
Claude managed agent (hosted session)SSE ↓ac-python-api · event relayassistant turn · tool_use · tool_result · message_stopThe hosted loop runs on Anthropic infra. We never see the loop - only the events it streams. The relay answers tool_use by running the enrichment fan-out (enrich_person / enrich_company) and posting tool_result back into the same session, and mirrors the user-visible subset into Supabase for audit.
ac-python-api · SSE fan-outSSE ↓ac-frontend · EventSourceinbox.insert · inbox.update · agent.progress · agent.doneOne LISTEN consumer + the relay's in-process agent.progress feed merge into a pub/sub fanned to N browser tabs. The frontend keys every frame on inbox.id and upserts - inserts prepend a warm-lead card, updates patch it in place. Same frame contract company monitoring already speaks.
tool_use by running the enrichment fan-out and posting tool_result back into the live session, mirrors the user-visible subset into Supabase, and writes the person_signal_inbox row. Everything downstream reads from that row - so the warm lead reaches the tab through the identical frame schema as a company signal. Downstream frame schema what each frame does to the Pinia store
inbox.insert{ id, person, kind, score, status:"new" }Prepend a new warm-lead card (no-op if id already present - replay-safe).agent.progress{ id, phase:"enriching", note }Patch the card to a live "enriching…" state while the agent works.agent.done{ id, company, rationale, draft }Fill in the new company + warm-intro draft on the existing card.inbox.update{ id, status:"read"|"dismissed"|"actioned" }Patch status in place - a write from any surface or tab lands here.Lifecycle invariants the rules the frontend leans on
Watch signals
interactiveThe same shared signal vocabulary as discovery, seen through a monitoring lens - each kind has a detection source and a re-check cadence. Pick a kind to see how it is watched and why it matters.
job change
high severityA watched champion moved companies. The flagship warm lead - follow them to the new account.
Warm-lead matcher
playgroundWhen an event fires on a watched person, the match plane scores it against the agency's rule: cohort weight × signal weight × recency × ICP fit, gated by a tunable threshold. Set up a fired signal and watch it land in drop, inbox, or hot.
Hot warm lead - inbox + instant Slack / digest priority.
cohort × signal × recency × fitIllustrative of the deterministic rule - exact weights and decay are tuned at build. The point: a champion's fresh job change clears almost any threshold; a cold-watch tenure nudge rarely does.
Schema additions
plumbingThree additions on top of the people intelligence layer. Events are global; watchlists and the inbox are tenant-scoped behind RLS.
person_signal_eventsglobalAppend-only. person_id · signal_type · observed_at · source_url · substantiated. One row per detected transition; collectors only insert.
person_watchlistsper-orgorg_id · person_id · cohort (champion / contact / target_dm / cold) · added_at. The set each agency watches and why.
person_signal_inboxper-orgorg_id · event_id · match_score · verdict · state (new / actioned / dismissed). The warm-lead feed the rep works.
Collector cadence
celery beatRe-check frequency follows signal volatility and cohort value, on celery beat - the volatile, high-value cohorts are watched hardest.
| Collector | Cadence | Diff strategy |
|---|---|---|
| Champion job / title diff | daily | LinkedIn URL re-crawl vs last snapshot + news corroboration |
| Contact job / title diff | every 2-3 days | LinkedIn URL re-crawl, lower priority queue |
| Target-account DM scan | daily | company leadership page + news for new buyer roles |
| Content / posting feed | 12h | RSS + social poll, new items since last cursor |
| Event / speaking scan | weekly | event-site crawl for watched names |
| Tenure milestone sweep | weekly | derived from role start dates, no fetch |
Delivery fan-out
routingThe inbox is the source of truth; everything else is a notification on top of it.
Signals inbox
Every survivor lands here as a warm lead with the person, the event, the match score, and a suggested outreach reason. The rep works the feed.
Slack & digest
Hot leads ping Slack instantly; everything else rolls into a daily digest so the inbox is not a firehose.
Webhook / CRM
A webhook lets agencies route warm leads into their own outbound tooling; promotion writes the person + signal into CRM.
Monitoring stores and processes personal data on an ongoing basis. Each watched person needs source lineage for Article 14 disclosure and a takedown path, and the watch itself must respect lawful-basis limits. Provenance and deletion are build requirements, not follow-ups.
Open questions
unresolvedFour questions to resolve before this leaves the design stage. Each carries a provisional answer.
How big can a watchlist get per agency before refresh cost bites?
Provisional: quota by plan, with the champion cohort always free to watch.
How far back should job-change backfill look on first import?
Provisional: 90 days, so a recent move is not missed at onboarding.
How is a job change detected reliably without a paid LinkedIn feed?
Provisional: LinkedIn URL re-crawl diff + news corroboration through the substantiation gate.
Do people signals decay out of the inbox, or persist until actioned?
Provisional: decay the score, persist the row - a warm lead stays visible but sinks.