RUDRESH.
← All work
02

Signal Scoring Engine

A Python CLI that rejects accounts before it scores them, decays every signal by its own half-life, and shows its arithmetic. Demonstrated on synthetic data, with the source public.

Built on my own time · Oct 2026
Python · TOML · JSON · CSV · pytest · Ruff · GitHub Actions

56tests, on Python 3.11, 3.12 and 3.13
3ordered rejection gates
6 of 13synthetic accounts that cleared the gates
0runtime dependencies outside the standard library

The problem

Two questions have to be answered in a different order than most systems answer them.

Should this account be considered at all, and only then, among the ones that survive, what gets worked first. Most scoring systems collapse those into one number, which means an account with a disqualifying problem can still outrank a clean one because it has a loud signal.

So this engine refuses first and ranks second.

The system

A Python CLI. Account records come in as JSON, scoring rules as TOML. Nothing else is required: there are no runtime dependencies outside the standard library.

Load and validate, fail fast. Invalid dates, duplicate account IDs, missing fields and unknown signal types stop the run rather than scoring around the problem.

Three ordered rejection gates, and the first failure is the reason.

  1. icp_fit — industry, employee count or region outside the ICP
  2. tool_conflict — a conflicting tool category, used as a proxy for an outbound motion that already exists
  3. signal_freshness — no signal on or before the as-of date, or every signal older than the freshness limit

Only accounts that pass get scored. A rejected account keeps its reason and gets no rank.

Every signal decays by its own half-life.

points = weight × 0.5 ^ (age_days / half_life_days)

Signals are classified against a fixed as-of date as fresh, stale or future-dated, and only fresh signals score. The default half-lives are illustrative: 30 days for an SDR job posting, 90 days for a funding round. A funding round stays interesting four times longer than a job posting, and the formula says so rather than a human remembering to discount it.

Combination bonuses apply once, and only when every required signal type is among the fresh ones. funding_round plus sdr_job_posting is one. new_sales_leader plus sdr_job_posting is another.

explain prints the arithmetic for one account: which gates it passed, what each signal contributed after decay, and which bonus fired. On the sample set at an as-of date of 2026-10-01, six accounts qualified and seven were rejected. ACC-001 ranked first on 66.65 points: 51.65 from decayed signals plus a 15.00 combination bonus. ACC-009 was rejected at tool_conflict despite fresh funding and fresh SDR hiring, which is the whole argument of the engine in one row.

Output is a terminal table, a per-account explanation, and CSV.

The decisions

Rejection is a gate, not a penalty

A low score and a disqualification are different facts and they should not share a scale. Subtracting points for a conflicting tool means a big enough funding signal can buy its way past it. An ordered gate cannot be outvoted.

The score has to be reproducible by hand

The model never invents the number because there is no model. Each signal contribution is rounded to two decimals once, and the total sums the values you can see, so a printed score reconciles with a calculator. Ranking is descending score with the account ID as the tie-breaker, so the same input always produces the same order.

--as-of replays the whole run at any date. That is what makes the decay auditable: you can ask what this account looked like three weeks ago and get the same answer twice.

Future-dated signals earn nothing

A signal dated after the as-of date scores zero points, fails the freshness check and fires no bonus. Dirty data in a signal feed usually arrives as a date in the wrong place, and the engine treats that as absent rather than as fresh.

The tests check the arithmetic, not the opinion

56 tests pass on Python 3.11, 3.12 and 3.13, covering score reconciliation and gate coverage. Ruff handles lint and format. GitHub Actions runs all three versions plus a CLI smoke test on every push.

Those tests validate software behaviour. They say nothing about whether the weights are right, which is a different problem and an honest limitation rather than a feature.

What this is not

This matters more than anything above, so it is stated plainly rather than buried.

It is a clean-room demonstration on synthetic data: 13 fictional accounts on .example domains. The sample cannot support any conclusion about a market, a conversion rate or a business outcome.

  • The weights, half-lives, thresholds and bonuses are illustrative and uncalibrated. Nothing here has been fitted against historical conversion data.
  • No live collection and no enrichment. No scrapers, no enrichment providers, no APIs, no LLMs, no secrets.
  • No CRM connection, no scheduler, no web service, no persistence.
  • ICP fit is pass or fail, not a graded score, and industry, region and tool categories match on exact strings.
  • No signal deduplication or saturation. A repeated signal contributes again each time.
  • No measured business impact. None is claimed.

What it does demonstrate is the shape of a scoring system: that disqualification belongs in a gate, that signal age belongs in the arithmetic rather than in someone’s judgement, and that a score nobody can reconstruct is a score nobody will trust.

The source

The repository is public, and the two documents worth reading are the case study and the architecture note.

It is the only project on this site whose source you can read, which is the point of publishing it. Everything else here is described. This one can be checked.

Next

Reply classification and routing

A scheduled service that reads cold reply traffic, registers genuine positives against the event, and routes anything ambiguous to a human instead of guessing.