# REFERRALS.md scoring, version 2.0

Every formula the referrals.md tools use, as implemented, with its variables, its defaults and a worked example. Scores compare the records a reader has, under the reader's own weights. They are never an endorsement, a verification or a guarantee of anyone. Every tool states its formula in its result, and the exact inputs and outputs below are in https://referrals.md/vectors.json, generated from the code and checked by a test. The same code runs on your own machine as https://referrals.md/referrals.mjs.

Dates are YYYY-MM-DD. "Days" are whole days between two dates. `as_of` is the date a score is measured on (default today). Numbers are rounded only where a step says so.

## 1. Tie strength

How well two parties know each other, from 0 to 1. Tool: `tie_strength`; also used inside `find_helpers` and `mutual_connections`.

```
strength = (w_recency x r + w_frequency x f + w_duration x d + w_reciprocity x q + w_context x c) / (sum of the weights), rounded to 4

r = 0.5 ^ (days from last to as_of / tie_half_life_days)
f = min(1, ln(1 + n) / ln(1 + frequency_cap))
d = min(1, (days from since to as_of / 365.25) / duration_cap_years)
q = 2 x min(initiated_by_a, initiated_by_b) / (initiated_by_a + initiated_by_b)
c = the context's weight
```

| Variable | Meaning | Default |
| --- | --- | --- |
| `w_recency`, `w_frequency`, `w_duration`, `w_reciprocity`, `w_context` | The weights | 0.3, 0.25, 0.15, 0.1, 0.2 |
| `last`, `since` | The latest and the first interaction | from the connection line |
| `n` | `interactions`, or `initiated_by_a + initiated_by_b` when interactions is not written | |
| `tie_half_life_days` | Days after which recency halves | 180 |
| `frequency_cap` | Interactions at which frequency reaches 1 | 100 |
| `duration_cap_years` | Years at which duration reaches 1 | 10 |
| context weights | family 1, friend 0.9, partner 0.9, colleague 0.8, client 0.7, vendor 0.7, other 0.4, met 0.3 | |

A part with no data counts 0. A date after `as_of` counts as `as_of`. Client and vendor share one weight, so a tie weighs the same whichever way round it is written.

**Worked example.** A buyer's agent and a home inspector, as of 2026-10-22: context colleague, since 2021-05-10, last 2026-10-15, 22 interactions, 15 started by the agent and 7 by the inspector.

```
r = 0.5 ^ (7 / 180)                 = 0.9734
f = ln(23) / ln(101)                = 0.6794
d = (1991 days / 365.25) / 10       = 0.5451
q = 2 x 7 / 22                      = 0.6364
c = colleague                       = 0.8
strength = 0.3 x 0.9734 + 0.25 x 0.6794 + 0.15 x 0.5451 + 0.1 x 0.6364 + 0.2 x 0.8
         = 0.2920 + 0.1698 + 0.0818 + 0.0636 + 0.1600 = 0.7673
```

## 2. Path strength

How warm a path of ties is, from 0 to 1.

```
path_strength = the product of the tie strengths along the path, rounded to 4
```

A path is a simple path of ties from the asker to a have's party, at most `max_hops` ties long (default 3, at most 6). It never passes through a party marked `do_not_refer`. The search stops after 200000 steps and says so.

**Worked example.** A home buyer (DK) needs a kitchen remodel. DK's ties, as of 2026-10-22: DK to their buyer's agent MJ 0.7119, MJ to an architect PF 0.8658, PF to a general contractor BN 0.8525, and MJ to BN directly 0.4958 (they met once).

```
DK > MJ > PF > BN:  0.7119 x 0.8658 x 0.8525 = 0.5254
DK > MJ > BN:       0.7119 x 0.4958          = 0.3530
```

The longer path is warmer: two strong ties beat one weak one.

## 3. Have fit

How well a have fits a need, out of 100, rounded to 1 place. Tool: `find_helpers`.

| Part | Points |
| --- | --- |
| Category | 40 when the have's category is the need's; 20 when the need's category is one of the have's tags; otherwise the have is not a candidate. |
| Requirements | 20 x met / wanted, the have's category and tags counting; 20 when the need lists none. |
| Area | 15 for the need's own area, a remote need, or no area given; 7.5 for a remote have or unknown areas. |
| Room on `as_of` | 0 when load is at or above capacity; 7.5 when away until a date before the need's `by_date`; 7.5 for available limited; 15 for available yes, or a known load below capacity; 7.5 when unknown. The party's capacity and load are used when the have has none. |
| Budget | 10 when the ranges overlap; 5 when the gap is within `tolerance_pct` (default 20) of the need's nearest bound, or a range is missing. |

Left out, with the reason shown: the asker's own have, a party marked `do_not_refer`, a party that avoids the category or a requirement, available no, and away until on or after the need's date. A have needs `min_fit` (default 50) to be a result.

**Worked example.** The kitchen need: renovation, budget 30000-60000, example-city, requirements kitchen and permits.

```
reno-bn (gc-bn):          40 + 20 x 2/2 + 15 + 15 (load 2 of 3) + 10 (overlap) = 100
reno-kc (contractor-kc):  40 + 20 x 1/2 + 15 + 15 (load 2 of 4) + 10           = 90    (no permits tag)
reno-rm (remodeler-rm):   40 + 20 x 2/2 + 15 + 7.5 (limited)   + 10            = 92.5
reno-xy (gc-xy):          left out: asked not to be referred
```

## 4. Helper ranking

How `find_helpers` orders the haves and the paths to each.

```
direct path (one tie):  route_score = path_strength
warm path:              route_score = path_strength x factor, rounded to 4
factor = 0.5 + 0.5 x (c x score / 100 + (1 - c) x 0.5), rounded to 4
result score = fit x the best route_score, rounded to 2
```

The referrer is the second party on the path. `score` and its confidence `c` are the referrer's score (section 5) in the need's category, or overall when there are none in it. With no score the factor is 0.75. Results are ordered by score, then fit, then path strength, then fewer hops, then have id. The paths under each result are ordered by route score, then fewer hops, then the path's ids.

**Worked example.** MJ's referrer score is 93.1 with confidence 0.369 (section 5). The accountant RA has one declined referral: score 0, confidence 0.132.

```
MJ's factor = 0.5 + 0.5 x (0.369 x 0.931 + 0.631 x 0.5) = 0.8295
RA's factor = 0.5 + 0.5 x (0.132 x 0     + 0.868 x 0.5) = 0.7170

reno-bn via DK > MJ > PF > BN:  route 0.5254 x 0.8295 = 0.4358   score 100  x 0.4358 = 43.58
reno-kc via DK > RA > KC:       route 0.5254 x 0.7170 = 0.3767   score 90   x 0.3767 = 33.90
reno-bn via DK > MJ > BN:       route 0.3530 x 0.8295 = 0.2928   (the second path to the same have)
reno-rm, a direct tie:          route 0.2847               score 92.5 x 0.2847 = 26.33
```

The best match is not the closest party: RM is a direct tie but a weak one with little room, and BN is three ties away through people who know the work.

## 5. Referrer score

How the referrals a party gave went, out of 100, rounded to 1 place. Tool: `score_referrer`.

```
weight = 0.5 ^ (age_days / half_life_days) per referral (1 when undated)
counted    = referrals that named a candidate, except one the asker cancelled
answered   = counted referrals past introduced (accepted, declined, in_progress, completed, lost)
acceptance = weight of accepted, in_progress, completed and lost / weight answered
completion = weight completed / weight of accepted, in_progress, completed and lost
outcome    = (weighted mean rating - 1) / 4
score      = 100 x (w_a x acceptance + w_c x completion + w_o x outcome) / (w_a + w_c + w_o)
confidence = n_effective / (n_effective + min_referrals), where n_effective = the weight answered
```

An ask the referrer declined because nobody fit, and a referral the asker withdrew, are left out (`left_out` counts them): neither says anything about the referrer's picks, and an honest "I know no one" never lowers a score. A closed referral counts as how it ended: its `ended` line, which `lifecycle_step` writes at the close. Without one, a rating reads as completed, a cancelled date as cancelled, a finished or started date as taken on but not known to be completed (lost and completed both set `finished`), and anything else as declined. A part with no data is dropped with its weight. The score counts only with `min_referrals` answered referrals; below that, quote the count. Bands: strong from 75, fair from 50, else weak.

| Variable | Default |
| --- | --- |
| `half_life_days` | 180 |
| `w_a`, `w_c`, `w_o` (`weight_acceptance`, `weight_completion`, `weight_outcome`) | 1, 1, 2 |
| `min_referrals` | 3 |

**Worked example.** MJ gave two referrals, as of 2026-10-22: a mortgage referral on 2026-08-20, closed and rated 5, and an inspection referral on 2026-10-14, completed and rated 4.

```
weights: 0.5 ^ (63 / 180) = 0.7846 and 0.5 ^ (8 / 180) = 0.9697; n_effective = 1.7543
acceptance = 1, completion = 1
mean rating = (0.7846 x 5 + 0.9697 x 4) / 1.7543 = 4.447; outcome = (4.447 - 1) / 4 = 0.8618
score = 100 x (1 + 1 + 2 x 0.8618) / 4 = 93.1
confidence = 1.754 / (1.754 + 3) = 0.369; 2 answered, below 3, so the band is too_few
```

## 6. Receiver reliability

How reliably a party answers, finishes and is rated on the referrals it gets, out of 100. Tool: `score_referrer` with `as: receiver`.

```
counted    = referrals to the party that reached it (consent asked of it, or introduced), weighted as in section 5
answer     = weight answered (consent given, responded, or a status that shows an answer) / weight counted
completion = weight completed / weight taken on (accepted, in_progress, completed, lost)
outcome    = (weighted mean rating - 1) / 4
score      = 100 x (w_answer x answer + w_c x completion + w_o x outcome) / (the weights used)
```

A referral still unanswered fewer than `answer_within_days` (default 5) after it reached the party is waiting and left out; after that it counts as not answered. One the asker cancelled before the party answered is withdrawn and left out. A closed referral counts as how it ended, as in section 5. Weights default to 1, 1 and 2; confidence, the minimum count and the bands are as in section 5.

**Worked example.** A general contractor got four referrals, as of 2026-10-22: completed and rated 5 (2026-04-01), closed and rated 4 (2026-07-15), declined (2026-09-01), and one introduced two days ago.

```
weights: 0.5 ^ (204 / 180) = 0.4559, 0.5 ^ (99 / 180) = 0.6830 and 0.5 ^ (51 / 180) = 0.8217; the fourth is waiting
answer = 1, completion = 1
mean rating = (0.4559 x 5 + 0.6830 x 4) / 1.1389 = 4.4; outcome = (4.4 - 1) / 4 = 0.85
score = 100 x (1 + 1 + 2 x 0.85) / 4 = 92.5; confidence = 1.961 / 4.961 = 0.395
```

## 7. Connections in common

Who two parties both know. Tool: `mutual_connections`.

```
in one book:   common = the parties tied to both a and b (a and b left out)
               jaccard = common / the parties tied to either
               weighted_jaccard = sum of min(s_a, s_b) / sum of max(s_a, s_b) over the union
across books:  parties matched by ref, then a shared hashed contact, then the same id, each once
               jaccard = matched / (side a + side b - matched)
hash lists:    jaccard = hashes in both / hashes in either
```

`s_a` and `s_b` are the tie strengths to a and to b; a missing tie counts 0. A raw email or phone is hashed first and never kept. **Worked example**: a buyer's agent and a closing attorney who share one of the three parties tied to either have jaccard 1 / 3 = 0.3333, and 0.2362 weighted by tie strength.

## 8. Identity strength

How much identity evidence is on record for a party, out of 100, rounded to 1 place. Tool: `identity_strength`. This reads recorded claims and is not a verification: referrals.md checks none of them, and a high score says only that more is on record.

```
claim value = method weight x attester weight x stale factor
best(kind)  = the highest claim value of that kind
score       = 100 x sum(kind weight x best(kind)) / sum(kind weights)
```

| Variable | Default |
| --- | --- |
| Method weights | link, code, dns, file and document 1; vouch 0.6; self 0.5 |
| Attester weight | the reader's own per attester, 1 when not set; 0.6 (`self_factor`) when the party says it of itself |
| Stale factor | 0.5 when the claim is older than `stale_after_days` (730) |
| Kind weights | email 20, phone 20, domain 20, profile 15, document 15, org 10 |
| Levels | none at 0, low below 34, medium below 67, high from 67 |

A claim counts 0 with no method, attester or date, when its method does not fit its kind, or when it is dated after `as_of`.

**Worked example.** An accounting firm, as of 2026-10-22: a domain claim checked by DNS by a buyer's agent, an email claim confirmed by a link and stated by the firm itself, and a professional profile vouched for by a bookkeeper 965 days ago.

```
domain:  dns 1 x attester 1                 = 1    -> 20 x 1   = 20
email:   link 1 x said of itself 0.6         = 0.6  -> 20 x 0.6 = 12
profile: vouch 0.6 x attester 1 x stale 0.5  = 0.3  -> 15 x 0.3 = 4.5
score = 100 x (20 + 12 + 4.5) / 100 = 36.5, medium; no phone, document or org claim
```

## 9. Check it yourself

- https://referrals.md/vectors.json: the inputs and exact outputs for ids, hashed contacts, tie strength, path strength, helper ranking, the referrer score, receiver reliability and identity strength.
- https://referrals.md/referrals.mjs: the same code as one Node 18+ module. `run("find_helpers", vectors.helper_ranking.input)` returns the ranking above.
- https://referrals.md/spec.md: the protocol these scores read.

Use at your own risk. Terms: https://referrals.md/terms.
