PROOViD AML Docs
Transparency & defensibility

Scoring & decisioning — how a screen becomes a decision

This is the one authoritative page for how PROOViD AML turns a name you submit into a match classification, a 0–100 risk score and band, and a final Pass / Review / Fail decision. Nothing here is a black box: every number and every branch is drawn straight from the scoring engine, and §5 is a worked example you can reproduce yourself with two curl calls. For how the underlying watchlist data is built see How the data is built; for the response shape see API reference. Deeper source-of-truth notes live in the engineering docs docs/decision-criteria.md, docs/decision-matrix.md and docs/decision-priority.md, which this page consolidates.

Two different numbers, two different questions. A result carries a name-match score (0–1: is this the same person?) and a riskScore (0–100: how risky is this hit?). They are not the same scale — a perfect name match does not imply a high risk score. A head of state and a city mayor can both match at name-score 1.0 and land in different bands and different decisions, because risk is driven by who they are and what category they're in, not by how well the name matched.

1 · The pipeline, end to end

Every screen runs the same five ordered stages. Each stage's output is the next stage's input, and the response returns the artefacts of all five.

  1. Retrieval. Candidate entities are pulled from the watchlist corpus by a Postgres trigram (pg_trgm) name search, optionally widened by phonetic and nickname expansion. This stage is about recall — cast a wide net; precision is imposed downstream.
  2. Classification. Each candidate is judged on the name × date-of-birth axis into TruePositive (full name + exact full-DoB agreement), PotentialMatch (a real hit a human should confirm), or FalsePositive (ruled out — a different person). Only TruePositive and PotentialMatch are real and carry forward.
  3. Scoring. The real matches produce a single riskScore (0–100) and a riskBand (Minimal / Low / Medium / High) — §2.
  4. Decision. Each real match is resolved through the tenant's decision matrix to a base Pass / Review / Fail, then aggregated (worst wins) and adjusted by the band policy and floors — §4.
  5. Return. The response carries isMatch, score, classification, riskScore, riskBand, decision, the reasonCodes, and the contributing factors[] — the full audit trail of the four stages above.
flowchart LR
  R["1 Retrieval
pg_trgm (+ phonetic / nickname)"] --> C["2 Classification
name x DoB"] C --> CL{"TruePositive /
PotentialMatch /
FalsePositive"} CL -- "real only" --> S["3 Scoring
0-100 + band"] S --> D["4 Decision
matrix -> Pass / Review / Fail"] D --> OUT["5 Return
score, riskScore, riskBand,
decision, reasonCodes, factors[]"]

2 · The 0–100 risk score

Only real matches feed the score — TruePositive or PotentialMatch. A warning type the tenant has switched off is dropped before scoring, as if the hit never happened. Each surviving match contributes:

flowchart LR
  M["Real matches only
(TruePositive / PotentialMatch,
enabled warning types)"] --> C["contribution =
categoryWeight x countryWeight x matchConfidence"] C --> MAX["aggregate = MAX single contribution
(the worst hit governs, not the sum)"] MAX --> N["normalize: a full-confidence,
weight-1.0 match = the tenant's High threshold"] N --> S["clamp 0-100 -> riskScore"]
  • matchConfidence — the provider name-match score, clamped to 0–1.
  • categoryWeight — the tenant's weight for the match's canonical category (sanctions / pep / adverse_media); criminal-category hits use the dedicated CriminalWeight.
  • countryWeight — the tenant's weight for the matched entity's nationality.
The score is a MAXIMUM, not a sum. AML risk is governed by the single worst hit, not by how many weak signals pile up — five weak PEP overlaps never outscore one strong sanctions hit. The dominant contribution is normalized so that a full-confidence (1.0), full-weight (1.0) match lands exactly on the tenant's High threshold — which guarantees a single strong sanctions hit always reaches the High band — then the result is clamped to 0–100.

Banding

The score is bucketed against three tenant-configured thresholds (defaults shown):

ConditionBand
score ≥ HighThreshold (default 85)High
score ≥ MediumThreshold (default 60)Medium
score ≥ LowThreshold (default 30)Low
otherwiseMinimal

3 · Classification & evidence tiers

Name strength is judged by the fuzzy name scorer. A name whose confidence reaches the near-exact bar (NearExactNameSimilarity = 0.97) is treated as an exact / near-identical name; anything below it is a weaker, partial overlap. That name signal is then combined with the date-of-birth signal — a corroborated full-DoB agreement, a full DoB that mismatches beyond tolerance, or no usable DoB — to place a real match into one of four evidence tiers. The tier is the row dimension of the decision matrix in §4.

Evidence tierNameDate of birth
ExactNameExactDobExact / near-exact (≥ 0.97)Full DoB agrees — the confirmed tier.
ExactNameNoDobExact / near-exact (≥ 0.97)No corroborating full DoB.
WeakOrPartialNamePartial / weaker (< 0.97)No corroborating full DoB.
ExactNameDobMismatchExact / near-exact (≥ 0.97)Full DoB present on both sides but differs beyond tolerance (DM-3).
DM-3 — an exact name with a wrong DoB is surfaced, not silently cleared. Such a candidate used to be classified FalsePositive and dropped (it Passed by absence). It is now surfaced as a PotentialMatch in the ExactNameDobMismatch tier, so the tenant matrix decides it — Review by default — rather than the pipeline clearing it. A merely partial name with a differing DoB is still ruled out as a different person.
#368 — per-tenant DobMismatchSurfacing (🟡 PROVISIONAL). When an exact name carries a mismatching DoB, which hits surface is a per-tenant choice:
  • StrictExactName (default) — only a token-exact name surfaces a DoB-mismatch; a strong-but-partial name with a differing DoB is discarded. Byte-for-byte the historical behaviour.
  • HighScorealso surface a partial-name hit when the provider score clears the near-exact bar (DobMismatchHighScoreThreshold, default 0.97). This stops a watchlist entry that carries an extra middle name yet still scores ~1.0 from being silently dropped on a DoB difference — a human confirms instead.
It is flagged PROVISIONAL pending the exact-vs-partial name boundary, and on the wire it is a readable string — "StrictExactName" / "HighScore".

4 · The decision matrix

Each real, enabled match gets a base decision from an explicit matrix keyed by category × evidence-tier × multiplicity (single vs 2+ real matches). The authoritative outcome is the strongest base decision across all matches — precedence is always Fail > Review > Pass — after which the band policy and floors apply. Two structural invariants keep the matrix regulatorily safe: a watchlist cell (sanctions / criminal) can never resolve to Pass, and a Multiple cell is never softer than its Single sibling (2+ matches can only hold or escalate).

The shipped default grid (identical for single and multiple), where P is the tenant's unconfirmed-hit posture SanctionsUnconfirmedDecision — Review by default, Fail for a strict tenant:

CategoryExactName + DoBExactName, no DoBExactName, DoB mismatchWeak / partial
sanctionsFailFailPP
criminal / terrorFailFailPP
pep (all tiers)ReviewReviewReviewReview
adverse_mediaFailReviewReviewReview

The rules the default cells encode, and the floors applied on top of the aggregate:

RuleEffect
Confirmed sanctions hit (exact full-DoB agreement, or a TruePositive)Fail regardless of score. An exact name to a sanctioned entity is also Fail with no DoB (DM-1) — a sanctions entry that carries no DoB, common on OFAC, can never reach the ExactDob tier, so anything softer would clear the most common sanctions shape.
Unconfirmed sanctions hit (weak, no-DoB, not near-exact)The tenant's SanctionsUnconfirmedDecision posture — Review by default, Fail for a strict tenant. Never auto-clears to Pass.
Confirmed PEP match, with PepMatchFloorsToReview on (the default)Floors the decision to at least Review — a sitting head of state can never quietly Pass, even in a low band.
Criminal / terror with a near-exact nameFail even with no DoB (terrorism is high-stakes) — the sanctions confirmation path extended with a name-only trigger.
No real matchPass.

Review-policy modifiers

When the aggregate is Review (nothing forced a Fail), two tenant flags can move it — but an unconfirmed watchlist hit is always floored back to Review, never quietly Passed:

ConditionResult
EscalateHighToFail on and band = HighFail
AllowLowBandPass on and band ∈ {Minimal, Low}Passunless an unconfirmed sanctions / criminal-terror factor is driving the Review, in which case floored back to Review.
OtherwiseReview
The primary (decisive) factor named on the result is chosen by the tenant's DecisionPriorityOrder ladder (default sanctions > criminal > PEP1 > PEP2 > PEP3 > PEP4 > adverse media). The ladder only decides which match is the headline "why" — it never changes the Pass/Review/Fail outcome. See docs/decision-priority.md.

5 · A worked example you can reproduce

To keep this honest and reproducible we screen a well-known sanctioned public figure (Bashar al-Assad — on the actual OFAC / EU / UK lists, a public watchlisted subject, not a private individual) against the live sandbox. Two curl calls; run them and you get the same shape.

# 1 - register a throwaway sandbox tenant; copy the aml_test_ key from the response into AML_KEY
curl -s -X POST https://aml-screening.dloizides.com/v1/register \
  -H "Content-Type: application/json" \
  -d '{"companyName":"Docs Example","email":"you@example.com"}'
export AML_KEY="aml_test_the-key-from-step-1"

# 2 - screen a subject that hits a sanctions entry
curl -s -X POST https://aml-screening.dloizides.com/v1/screenings/check \
  -H "X-Api-Key: $AML_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fullName":"Bashar al-Assad"}'

The real response (captured 2026-08-09, abridged to the fields that matter):

{
  "isMatch": true,
  "score": 1,
  "classification": "PotentialMatch",
  "riskScore": 85,
  "riskBand": "High",
  "decision": "Fail",
  "matchedEntities": [
    { "sourceList": "UK",       "matchedName": "Bashar AL ASSAD",  "score": 1, "classification": "PotentialMatch", "rejectionTag": "sanctions" },
    { "sourceList": "EU",       "matchedName": "Bashar AL-ASSAD",  "score": 1, "classification": "PotentialMatch", "rejectionTag": "sanctions" },
    { "sourceList": "OFAC",     "matchedName": "Bashar AL-ASSAD",  "score": 1, "classification": "PotentialMatch", "rejectionTag": "sanctions" },
    { "sourceList": "WIKIDATA", "matchedName": "Bashar al-Assad",  "score": 1, "classification": "PotentialMatch", "rejectionTag": "pep" }
  ],
  "factors": [
    { "category": "sanctions", "sourceList": "UK",       "country": "Syria", "contribution": 1 },
    { "category": "sanctions", "sourceList": "EU",       "country": null,    "contribution": 1 },
    { "category": "sanctions", "sourceList": "OFAC",     "country": "Syria", "contribution": 1 },
    { "category": "pep",       "sourceList": "WIKIDATA", "country": "Syria", "contribution": 1 }
  ]
}

Stepping the real numbers through §2–§4:

StageWhat happens on this response
ClassifyThe submitted name is token-exact to Bashar AL-ASSAD, so it is a Full name match. No DoB was supplied, so it cannot reach full-DoB agreement → classification: PotentialMatch, not TruePositive. Name score 1.0 ≥ 0.97 and no DoB ⇒ evidence tier ExactNameNoDob.
ScoreEvery top factor's contribution = categoryWeight(1.0) × countryWeight(1.0) × matchConfidence(1.0) = 1.0. The aggregate is the maximum = 1.0. Normalized: 1.0 × HighThreshold(85) = 85riskScore: 85.
Band85 ≥ 85riskBand: High.
DecideThe sanctions factors sit in sanctions × ExactNameNoDob → matrix cell Fail (DM-1: an exact name to a sanctioned entity is confirmed even without a DoB). Aggregate precedence (Fail > Review > Pass) ⇒ decision: Fail. The PEP head-of-state hit alone would already floor to Review, but the sanctions Fail dominates.
The DoB-mismatch contrast (#368), in the abstract. Screen the same person with an extra middle name — a partial name — and a DoB that disagrees with the entity's (the well-known "exact name, wrong DoB" case). Under StrictExactName that partial-name DoB-mismatch is discarded as a FalsePositive and the screen would Pass; under HighScore, because the provider score clears 0.97, the same hit is surfaced as a PotentialMatch and the screen goes to Review. Same data, one tenant knob — this is exactly the gap #368 closes.
Sandbox registration is rate-limited; if step 1 returns 429, retry shortly. The numbers above are the real captured values — if the lists have moved since, your riskScore and top factors[] will still step through the formula identically.

6 · What's tenant-configurable

Every number and branch above is per-tenant configuration (RiskProfileConfig + WarningTypeConfig + the matching profile), not a fixed constant:

KnobDefaultWhat it changes
CategoryWeights[category]1.0Multiplier per canonical category (sanctions, pep, adverse_media) in the score contribution.
CountryWeights[isoCode]1.0Multiplier per matched entity's nationality.
CriminalWeight1.0Multiplier specifically for criminal-category hits (terrorism / bring-your-own criminal-records).
LowThreshold / MediumThreshold / HighThreshold30 / 60 / 85Band cut-offs (§2); High also anchors the score normalization.
WarningTypesall enabled, no overridesPer-warning-type kill switch (a PEP tier, an adverse-media category) and a tier/category → decision override.
SanctionsUnconfirmedDecisionReviewPosture P for an unconfirmed sanctions / criminal hit — Review or Fail. Never permits auto-Pass.
PepMatchFloorsToReviewonFloors a confirmed PEP match to at least Review, regardless of band.
DecisionPriorityOrdersanctions > criminal > PEP1–4 > adverse mediaWhich match is named PRIMARY; never changes the outcome.
DecisionMatrixOverridesnoneSparse per-cell overrides of the §4 grid (category × tier × multiplicity). Normalized so a watchlist→Pass is raised to Review and a softened Multiple is raised to its Single sibling.
DobYearTolerance1How many years two DoBs may differ and still count as agreeing (below this ⇒ corroborated; beyond ⇒ mismatch, feeding the tier logic in §3).
DobMismatchSurfacing 🟡StrictExactName#368 — whether an exact-name-with-wrong-DoB is surfaced strictly (token-exact only) or also for a high-scoring partial name (HighScore, gated by DobMismatchHighScoreThreshold, default 0.97). PROVISIONAL.
Honest nuance. The raw name-match confidence itself is engine-level — the DobYearTolerance and the DobMismatchSurfacing mode aside, making name-strictness fully per-tenant is a documented pending item (DM-5). Everything else on this page is live per-tenant configuration today, set via the console Settings panel or the tenant provisioning / matching-profile APIs — the same risk profile referenced throughout Coverage & sources.
© PROOViD AML · Developer Docs How the data is built · Coverage & sources · API reference