Skip to content

Fraud output contract (MOD-STD)

Single source of truth for ML/rule prediction shapes, validation, score normalization, and dashboard handoff. Implementation lives in src/report_team/schemas.py, validation.py, normalize.py, evidence.py, policy.py, and agent.py.

Related repos:

Repo Role Contract doc
fraud-detection-ai-backend-stream/ml_service Emits typed MlPredictionOutput ../fraud-detection-ai-backend-stream/docs/fraud-output-contract.md
fraud-detection-ai-backend-stream/rule_service Emits typed PredictionResult same
kyc-kyb-ai-backend KYC onboarding (separate domain) ../kyc-kyb-ai-backend/docs/kyc-output-contract.md

Pipeline

TransactionEvent → checks baselines → lastCheckReport
  → ml_service /call-and-predict  OR  rule_service
  → Pub/Sub → checks /report → compose_report → dashboard create-report

Report generation uses a hybrid model. The model may explain a fraud decision. It may never make the fraud decision.

prediction → validate contract → normalize score/category
          → derive evidence → evaluate fraud-decision-v1
          → LLM aiSummary only → invariant-checked report
Field Source
fraudScore, riskCategory Python (normalize.py)
indicatorRecords, aiChecks, reasonCodes Python (evidence.py)
decision, escalations Python (policy.py, fraud-decision-v1)
fraudIndicators, aiRecommendation Derived views of the structured decision (dashboard-compatible)
aiSummary Gemini, or a deterministic fallback if Gemini fails
provenance contract/policy/narrative versions + prediction hash

Routing uses envelope check_type plus mutually exclusive identity keys: ML = probability_score, rule = flagged + score. predicted_level is shared (ML and rule both emit it) and is not an identity key. Mixed identity (probability_score together with flagged+score) fails with AMBIGUOUS_PREDICTION_CONTRACT.

Decision policy (fraud-decision-v1, provisional)

Primary decision (approve | pend | block) is mutually exclusive. Escalations are separate (investigator_review, sar_review).

riskCategory Primary Escalation
low approve none
medium pend none
high block investigator_review if any triggered indicator

Named overrides (confirm with compliance):

Signal Effect
BLACKLISTED_* at least pend
SANCTIONS_* / KNOWN_FRAUD_* at least block
SAR_* / SAR_TRIGGER add sar_review

Blacklist cannot produce approve. Adding a material indicator cannot lower primary severity.

predicted_level must match the documented ML score bands or the payload is rejected (ML_SCORE_TIER_CONFLICT). This is contract enforcement, not a scoring-algorithm change (CH-SYNC).

Unflagged rule scores still map to riskCategory=low with fraudScore=round(score×100) and reason RULE_NOT_FLAGGED. Do not treat that as comparable to ML probability until a later product decision.


Input validation

TransactionEvent

Required for baseline checks. Validated via TransactionEvent in src/application/data_models/transaction.py.

Field Type Required Notes
id string yes Transaction UUID
time string yes HH:MM:SS
date string yes YYYY-MM-DD
amount float yes
currency string yes e.g. NGN
description string | null no
type string | null no e.g. bill payment
device string | null no
device_type string | null no e.g. Online
balance float | null no Account balance after txn

Gap: channel is not on TransactionEvent today. POL-01 documents omni-channel scaffolding; wiring is separate.

Use validate_transaction_event() before baseline checks when validating inbound payloads.

lastCheckReport

Minimum structure for ML /call-and-predict. Validated via LastCheckReportInput in schemas.py.

Field Required Notes
id yes Report id
customerProfileId yes
temporalChecks.prediction no (defaults {}) Dict of temporal anomaly flags
transactionChecks.prediction no
locationChecks.prediction no
*LastProcessedDate no ISO-ish date strings
createdAt no

Each check section stores a flat prediction dict of boolean anomaly keys consumed by scoring.


ML prediction output

Produced by fraud-detection-ai-backend-stream/ml_service (ThresholdLabelling / ModelInference in ml_service/src/models/scoring.py).

Field Type Description
probability_score float 0–1 Weighted anomaly score (old algorithm: sum weights / (sum − 32.5), cap 0.9)
predicted_level "Low Risk" | "Mid Risk" | "High Risk" Mapped from score bands: >0.66 High, >0.33 Mid, else Low
interpretation object Human-oriented explanations per check
observations object Per-check {status, description} flags

Pydantic model: MlPredictionOutput.

ML → dashboard normalization

fraudScore     = round(probability_score × 100)   # Python 3 banker's rounding
riskCategory   = high   if probability_score > 0.66
               = medium if probability_score > 0.33
               = low    otherwise

predicted_level must equal the band for probability_score. A payload with 0.90 and "Low Risk" is rejected (ML_SCORE_TIER_CONFLICT). The reporting layer does not silently recompute the producer's tier.

indicatorRecords use stable codes (e.g. BLACKLISTED_LOCATION_TRANSACTION). fraudIndicators is the list of those labels. interpretation is not used for evidence or policy. severity is set only when the observation code carries a known signal (UNUSUAL_* / BLACKLISTED_* → high, HIGH_* / LARGE_* → medium). Unknown codes omit severity rather than defaulting to low.

aiChecks is one item per {status, description} observation. Stakeholder status is triggered (observation true) or not_triggered (observation false). Producer passed/failed language is not used on the dashboard payload.


Rule prediction output

Produced by fraud-detection-ai-backend-stream/rule_service (rules_based_engine.py).

Field Type Description
flagged bool Whether any rule group failed
score float 0–1 Rule severity score
passed_conditions list Nested condition trees that passed
failed_conditions list Nested condition trees that failed
predicted_level "Low Risk" | "Mid Risk" | "High Risk" Advisory tier from the rule engine; ignored for identity and dashboard riskCategory

Pydantic model: RulePredictionOutput.

Rule → dashboard normalization

fraudScore     = round(score × 100)
riskCategory   = low    if flagged == false
               = high   if flagged && score >= 0.66
               = medium if flagged && score < 0.66

The high band is inclusive (>= 0.66) on the rule path. ML uses exclusive > 0.66. A flagged rule score of 0.659 can therefore round to fraudScore 66 with riskCategory medium.

fraudIndicators / aiChecks come from failed_conditions / passed_conditions (producer buckets). Those buckets mean predicate matched vs did not match, not “the analyst check passed.” Stakeholder aiChecks.status is therefore triggered (failed_conditions) or not_triggered (passed_conditions). Keep passed_conditions / failed_conditions only on rawAnalysisResponse.

Stakeholder labels describe the rule in business language (Transaction amount exceeds 5,000,000). Details separate observed value from threshold (20,000,000 was observed against a 5,000,000 threshold). Amounts are grouped with thousands separators. Currency symbols are not invented when the /report envelope has no currency. Missing actual_value is actual value was unavailable, never a silent pass.

indicatorRecords.code / reasonCodes remain stable machine identity (including condition UUIDs). severity is omitted unless the code matches a known prefix — UUID rule conditions must not show low as a fake default.


Dashboard output (AIReportResponse)

Final payload to {backend_api}/transaction/create-report:

Field Type Source
transactionId UUID Pub/Sub envelope
aiSummary string LLM
fraudScore int 0–100 Normalizer
riskCategory low | medium | high Normalizer (from score, not LLM)
fraudIndicators list[string] Labels derived from indicatorRecords
indicatorRecords list[FraudIndicator] Stable code/label/source; severity only when authoritative
aiRecommendation list[string] Derived [decision] + escalations
decision approve | pend | block Policy
escalations investigator_review | sar_review Policy
reasonCodes list[string] Policy + evidence codes
aiChecks list[FraudCheckResult] Python from observations / rule conditions; status is triggered | not_triggered
provenance object contract/policy versions, prediction hash, sourceScore
rawAnalysisResponse object Full Pub/Sub payload
timestamp string Server time

Merged model: FraudDashboardReport.


Risk tier mapping

Code (implemented today)

Three tiers everywhere scoring and dashboard agree:

Internal ML label (input, advisory) Dashboard riskCategory Score band (probability_score)
Low Risk low ≤ 0.33
Mid Risk medium > 0.33 and ≤ 0.66
High Risk high > 0.66

Dashboard riskCategory is derived from probability_score and must agree with predicted_level. Rule path uses flagged + score as documented above.

Product plan (not implemented)

Some planning docs reference Critical / High / Mid / Low (four tiers). That mapping is not in production code. Adding a Critical tier requires coordinated changes in ml_service, rule_service, dashboard UI, and this contract — tracked under CH-SYNC, not MOD-STD.


Checks-service vs ml_service drift (document only)

MOD-STD does not fix scoring drift. Gaps for CH-SYNC:

Area checks-service (fraud-detection-ai-backend) ml_service (stream)
Scoring algorithm Tier-group max per group (ANOMALY_TIER_GROUPS) in src/models/scoring.pynot wired into live checks path Legacy sum / (sum − 32.5) in ml_service/src/models/scoring.pylive ML path
constant.py weights Includes channel_velocity_anomaly, tier groups, CRITICAL_FEATURES Missing channel weight and tier-group constants
Critical override CRITICAL_FEATURES → score 1.0 in checks scoring module Blacklist keys hard-coded in anomaly_weighted_score()
Channel DB column + temporal helper scaffolded; not end-to-end No channel feature

Until CH-SYNC lands, ML predictions on /call-and-predict use the stream ml_service algorithm. Baseline generation in checks may diverge if the unused checks scoring module is enabled later.


Verification

Unit tests: tests/test_fraud_contract.py

  • Route detection (ML vs rule); mixed bodies rejected
  • Score/tier bands; ML score/tier conflicts rejected
  • Observation-derived indicator codes and checks
  • Versioned decision policy, monotonicity, blacklist cannot approve
  • Identical predictions: only aiSummary may differ
  • Provenance (fraud-output-v3, fraud-decision-v1)

Run from repo root:

cd fraud-detection-ai-backend
uv run python -m unittest tests.test_fraud_contract

Ticket Scope
MOD-STD Contract, validation, normalization, structured LLM narrative
CH-SYNC Align checks-service and ml_service scoring + constants
CH-BASELINE Historical replay (blocked on MOD-STD)
POL-01 Channel field end-to-end wiring