# Signals and scoring

Depth for procedure steps 2 and 5. The planned signal-based-selling-playbook and lead-scoring-model skills will go further; until they are installed, this file holds the essentials.

## Observation first, inference second

- Store the observed fact apart from the hypothesis. Fact: "a job ad on 2026-10-20 mentions a warehouse migration". Hypothesis: "they may need a reporting layer". The hypothesis is never written into the CRM as fact.
- CRM wording rule: the task or note quotes the observation, its source URL or record, and its date. "Interested in our product" is not evidence.
- A message may only claim what the evidence shows for that person. An account-level signal never becomes "you visited", "you downloaded" or "you are evaluating".

## Signal classes and action ceilings

- Direct request (demo, pricing, contact sales, reply asking for a call): a human response that never waits for enrichment or a score. It has no expiry; an unanswered request becomes an SLA failure.
- First-party (authenticated product event, account milestone, billing event, webinar attendance by a known contact): an owner task. A sequence only for unowned new business, through the step 7 gate.
- Inferred (IP-to-company web intent, third-party intent topics): account research or an owner task. Never a person-level claim and never a sequence on this signal alone. Corroboration means a second, person-level fact (a known contact's own action, a human review) recorded with its source.
  - Why: Clay's website tracking help (checked 2026-11-01) warns that bots, VPNs, cloud infrastructure and ISP-owned IP ranges can appear as companies, and that secondary or infrastructure domains can be matched. HubSpot buyer intent is defined at company level.
- Public change (hiring, funding, leadership or job change, technology change): research or owner review. A sequence only after the step 7 gate, with a dated source for every claim.

## Event key and episode

- Event key = source plus the source's own event ID (form submission ID, product event ID, provider signal ID). CloudEvents (Apache-2.0) uses the same idea: a producer keeps source plus id unique per distinct event, so a consumer can treat a repeat of both as a duplicate.
- No source ID: hash source, entity ID, signal type and `occurred_at`. Never hash the payload, which changes on retries.
- Episode = account plus play within the expiry window. Keep `first_seen_at`. A repeat of the same news from a second provider adds an evidence reference to the open task; it never refreshes urgency or creates a second task.
- Delivery dedupe (event key) protects against webhook retries; business dedupe (episode) protects reps from the same story twice.

## Three clocks

- `occurred_at`: when it happened at the source (form submitted, job posted, session ended).
- `observed_at`: when the workflow received it. A large gap means a slow or stale source.
- `actioned_at`: when the owner acted. A large gap from `observed_at` means a slow queue or a slow owner.
- Never use collection time as event time. A job change imported today may have happened eight months ago.

## Expiry defaults

- Product signals 7 days; web intent 14; hiring and funding 30 days from the source's own date; job change 90 days from the start date. Unknown source date: research only.
- Tune by sales cycle: shorten for fast transactional motions, never extend past the point where the evidence still describes the current situation.
- At action time, `evidence_age_days` must be at or below `expiry_days` (invariant I7 for sequences, I12 for tasks and nurture).

Fictional signal record:

```json
{
  "source": "clay_job_change",
  "event_id": "jc-4163",
  "signal_type": "job_change",
  "account_id": "A-1009",
  "identity_basis": "linkedin_url_match",
  "occurred_at": "2026-10-07",
  "observed_at": "2026-10-28T06:10:00Z",
  "eligible_until": "2027-01-05",
  "evidence_ref": "https://www.example.com/team/new-hire-announcement",
  "rule_version": "signals-v3",
  "fit_status": "complete",
  "recommended_action": "sequence_if_gate_passes",
  "activation_state": "pending_gate"
}
```

## Vendor notes (checked 2026-11-01; verify live before quoting)

- Clay job change signal: 1 Action plus 0.2 Data Credits per check, keyed on a LinkedIn profile URL. The optional initial check can add a row when the current company already differs, which may be an old change; read the start date before treating it as news.
- Clay website tracking: completed sessions appear with up to 30 minutes delay, grouped by company domain. Monthly caps stop new sessions until reset; running out of credits or reaching the 50,000-row table limit disables the connection until someone re-enables it. Monitor the connection state, not only the row count.
- Clay custom signals: compare structured values deterministically first (headcount moved, field changed) and use AI interpretation only on what that comparison passes.
- HubSpot buyer intent: company-level criteria (page paths, minimum visits and visitors, time period, exclusions); adding and tracking companies consumes HubSpot credits; auto-add covers companies entering saved views after it is enabled. Check credit amounts live.
- ZoomInfo company signals (intent, news, scoops) draw data credits for companies not already under management. Confirm against the contract.

## Monitoring cost counts checks, not changes

- Fictional math with the Clay job change rate: 100 people checked weekly for 4 weeks = 400 checks = 400 Actions and 80 Data Credits. Checked daily = 2,800 checks = 2,800 Actions and 560 Data Credits.
- Choose daily only when detecting a change six days earlier changes the action. Most job-change plays do not need it.

## Rep attention budget

- Weekly capacity = reviewers x minutes available per week / minutes per review (default 8 minutes until the team measures its own).
- Example: 2 reviewers x 90 minutes / 8 = 22 tasks per week (round down).
- Overflow expires with a reason code. It never spills into a sequence to "use" the signal.

## Scoring structure

- Gates outside the points: suppression, customer, open opportunity, active conversation, unsupported use case and excluded region route directly (step 6). Negative points let a strong engagement score cancel a suppression; that is a FAIL (W10).
- Fit and engagement are separate totals with separate conditions and windows, both visible to the rep. One composite hides why a record qualified.
- Each condition counts once per window. Ten page views by one person are one condition met, not ten.
- Unknown earns no points and sets `fit_complete = false`. Incomplete records go to research or clarification, never to rejection.
- Never score email opens. Apple Mail Privacy Protection loads remote content, including tracking pixels, without a person reading the message. Score identified traffic only, and count distinct humans, not events.
- Direct requests bypass the score.
- Band from capacity: set the proactive threshold so the weekly band fits capacity. If it does not, raise the threshold or rank by latest qualifying signal, then stable account ID as the tie-breaker.
- Store score version, components and reason codes with every routed decision.

```yaml
score_spec:
  version: acme-score-v2
  fit:
    window_days: 180
    conditions:
      - {id: F1, rule: "employee_count between 50 and 1000", points: 15}
      - {id: F2, rule: "industry in approved list", points: 10}
      - {id: F3, rule: "data warehouse in technology list", points: 15}
      - {id: F4, rule: "region in served regions", points: 10}
    unknown: no points, set fit_complete false
  engagement:
    window_days: 14
    conditions:
      - {id: G1, rule: "trial collaboration milestone reached", points: 20}
      - {id: G2, rule: "two or more identified people active this week", points: 10}
      - {id: G3, rule: "pricing page viewed by an identified contact", points: 5}
    excluded: [email_open, unidentified_page_view]
  gates_outside_points: [suppressed, customer, open_opportunity, active_conversation, excluded_region]
  proactive_band: {fit_min: 35, engagement_min: 20, weekly_capacity: 22}
  tie_breaker: [latest_signal_at desc, account_id asc]
```

## Validating a score (summary)

- Use point-in-time snapshots: the score as it was on the decision date, never today's values.
- Wait for a completed outcome window before judging.
- Report precision at capacity, recall and lift. Fictional example: 800 eligible accounts, 40 later reached the outcome (base rate 5%). The top 80 by score held 12 of them: precision 15%, recall 30%, lift 3 times.
- Lift is concentration, not causation. Depth and weight fitting belong in lead-scoring-model.

## Evaluating AI research before it gates outreach

- Output contract: supported, contradicted or insufficient evidence, with source URL and observed date. "Insufficient" is a valid answer.
- Build a labeled set of at least 50 accounts with near misses and insufficient-evidence cases. Slice by subsidiaries, stale postings, ambiguous names and small companies.
- Default gate: at least 80% precision against human reviewers before an AI result can gate outreach. Below that it can only prepare research for a person.
- Count precision only over accounts the AI marked supported, and require at least 30 of them. 80% on 20 supported accounts has a 95% interval of about 58% to 92%.
- Record the agent, prompt and model version per run, so a change can be re-evaluated.

## Optional: incrementality

- Proactive plays can use a randomized account-level holdout; never hold out direct requests.
- With fewer than 30 outcomes per arm, report the result as inconclusive.
