Fraud Intelligence Service — Event Schemas
Version: 1.0 Status: Draft Owner: Trust and Safety Last Updated: 2026-04-21 Companion: DOMAIN_MODEL · SYNC_CONTRACT · SECURITY_MODEL · docs/01-enterprise-architecture §6
All events are published to NATS JetStream via the transactional outbox pattern (see DATA_MODEL §6). Every event carries schemaVersion, eventId (UUIDv4), traceId, at (RFC 3339). Event bodies are PII-safe: raw MSISDNs are emitted only when scoped to the same tenant; cross-context events use msisdnHash = sha256(msisdn + tenantSalt). Raw SMS body content is never emitted in any fraud event.
1. Streams, subjects, retention
| Stream | Subjects | Retention | Replicas | Dedup window |
|---|---|---|---|---|
FRAUD_EVENTS | fraud.detected.> | 90 days | 3 | 2 min |
FRAUD_CASES | fraud.case.> | 13 months (regulatory) | 3 | 2 min |
FRAUD_TENANT_SCORE | fraud.tenant_score.> | 365 days | 3 | 2 min |
FRAUD_MODEL | fraud.model.> | 365 days | 3 | 2 min |
FRAUD_FEED | fraud.feed.> | 365 days | 3 | — |
FRAUD_ALERT | fraud.alert.> | 90 days | 3 | 2 min |
FRAUD_AUDIT | fraud.audit.v1 | 13 months (regulatory) | 3 | 2 min |
All streams have a corresponding .deadletter suffix. Consumers are durable with explicit ack per PLT-REQ-008/009.
2. Produced events
2.1 fraud.detected.ait.v1
Emitted by the AIT XGBoost pipeline (UC-03) when score ≥ 0.85.
interface FraudDetectedAit {
schemaVersion: '1';
eventId: string; // UUIDv4
detectionId: string; // fd_…
category: 'AIT';
subjectScope: 'TENANT' | 'SENDER_ID';
subjectId: string; // tenantId or senderId
score: number; // 0..1
confidenceTier: 'HIGH'; // ≥0.85 only for this subject
windowStart: string; // RFC 3339
windowEnd: string;
evidence: {
submitCount: number;
dlrSuccessRate: number;
uniqueDstMsisdns: number;
repeatedBodyRatio: number;
cohortAnomalyScore?: number;
sampleEventIds: string[]; // up to 50
};
aiProvenance: {
modelId: string; // ml_…
modelVersion: string; // semver
pipeline: 'XGBOOST';
trainingSetHash: string; // sha256
featureSetHash: string; // sha256
shapTop3: Array<{ feature: string; value: number; contribution: number; }>;
runtimeMs: number;
};
suggestedAction: 'THROTTLE_TENANT' | 'SUSPEND_SENDER_ID' | 'NO_ACTION';
traceId: string;
at: string;
}
JSON Schema (excerpt):
{
"$id": "https://schemas.ghasi.af/fraud/detected/ait/1",
"type": "object",
"required": ["schemaVersion","eventId","detectionId","category","subjectScope","subjectId","score","confidenceTier","windowStart","windowEnd","evidence","aiProvenance","traceId","at"],
"properties": {
"score": { "type": "number", "minimum": 0.85, "maximum": 1 },
"category": { "const": "AIT" },
"confidenceTier": { "const": "HIGH" }
}
}
2.2 fraud.detected.ait_ring.v1
Emitted by the cohort job (UC-04) when a cross-tenant ring is detected.
interface FraudDetectedAitRing {
schemaVersion: '1';
eventId: string;
detectionId: string;
category: 'AIT_RING';
cohortHash: string; // sha256
contributingTenants: string[]; // tenant IDs
contributingSenderIds: string[];
dstMsisdnCount: number;
windowStart: string;
windowEnd: string;
cohortAnomalyScore: number; // 0..1
aiProvenance: {
modelId: string;
modelVersion: string;
pipeline: 'GRAPHSAGE';
trainingSetHash: string;
featureSetHash: string;
runtimeMs: number;
};
suggestedAction: 'BLOCKLIST_COHORT';
traceId: string;
at: string;
}
2.3 fraud.detected.simbox.v1
interface FraudDetectedSimbox {
schemaVersion: '1';
eventId: string;
detectionId: string;
category: 'SIMBOX';
msisdnBlock: string; // CIDR-style "+93790000/28"
peerAsn?: number;
windowStart: string;
windowEnd: string;
evidence: {
msisdnRangeDensity: number;
bodyTemplateHashConcentration: number;
hlrMismatchRate: number;
imsiUniqueCount: number;
mnoBindConcentration: number;
};
aiProvenance: {
modelId: string;
modelVersion: string;
pipeline: 'IFOREST' | 'RULE_PATTERN';
trainingSetHash: string;
featureSetHash: string;
runtimeMs: number;
};
suggestedAction: 'QUARANTINE_MSISDN_BLOCK';
expiresAt: string; // 7d default
traceId: string;
at: string;
}
2.4 fraud.detected.simbox_network.v1
interface FraudDetectedSimboxNetwork {
schemaVersion: '1';
eventId: string;
detectionId: string;
category: 'SIMBOX_NETWORK';
peerAsn: number;
detectionIds: string[]; // member fraud.detected.simbox.v1 IDs
windowStart: string;
windowEnd: string;
suggestedAction: 'DEPEER_PEER_ASN';
traceId: string;
at: string;
}
2.5 fraud.detected.otp_harvesting.v1
interface FraudDetectedOtpHarvesting {
schemaVersion: '1';
eventId: string;
detectionId: string;
category: 'OTP_HARVEST';
tenantId: string;
senderId?: string;
cohortHash: string;
evidence: {
otpCount: number;
revocationCount: number;
revocationRate: number;
cohortSize: number;
};
aiProvenance: { modelId: string; modelVersion: string; pipeline: 'RULE_PATTERN' | 'XGBOOST'; trainingSetHash: string; featureSetHash: string; runtimeMs: number; };
suggestedAction: 'SUSPEND_SENDER_ID';
traceId: string;
at: string;
}
2.6 fraud.detected.otp_grinding.v1
Emitted by the streaming detector (UC-08) within 5 s of threshold crossing.
interface FraudDetectedOtpGrinding {
schemaVersion: '1';
eventId: string;
detectionId: string;
category: 'OTP_GRINDING';
dstMsisdnHash: string; // sha256(msisdn + nationalSalt)
windowStart: string; // 60s window start
windowEnd: string;
otpCountInWindow: number; // > 10
srcTenants: string[];
srcSenderIds: string[];
recommendedThrottle: { rateLimit: '1per60s'; durationSeconds: 21600 };
traceId: string;
at: string;
}
2.7 fraud.detected.greyroute.v1
interface FraudDetectedGreyroute {
schemaVersion: '1';
eventId: string;
detectionId: string;
category: 'GREY_ROUTE';
peerId: string;
peerAsn: number;
windowStart: string;
windowEnd: string;
evidence: {
totalMt: number;
mtToPeeredMno: number;
mtToNonPeeredMno: number;
mtToNonPeeredRatio: number;
hlrMismatchRate: number;
dlrSuccessRateAnomaly: number;
};
confidence: number;
suggestedAction: 'QUARANTINE_PEER' | 'OPEN_CASE';
traceId: string;
at: string;
}
2.8 fraud.detected.dlr_anomaly.v1
Feature-level event consumed by downstream models, not a terminal verdict.
interface FraudDetectedDlrAnomaly {
schemaVersion: '1';
eventId: string;
tenantId: string;
dstMno: string;
windowStart: string;
windowEnd: string;
baselineMean: number;
observedMean: number;
sigmaDeviation: number; // > 3 to emit
direction: 'HIGH' | 'LOW';
traceId: string;
at: string;
}
2.9 fraud.detected.dlr_uniformity.v1
interface FraudDetectedDlrUniformity {
schemaVersion: '1';
eventId: string;
tenantId: string;
windowStart: string;
windowEnd: string;
p50Ms: number;
p95Ms: number;
p99Ms: number;
stddevMs: number; // < 100 to emit
sampleCount: number; // > 500 to emit
traceId: string;
at: string;
}
2.10 Case lifecycle: fraud.case.opened.v1, fraud.case.decided.v1, fraud.case.auto_stale.v1, fraud.case.action_dispatched.v1
interface FraudCaseOpened {
schemaVersion: '1';
eventId: string;
caseId: string;
category: string; // FraudCategory
subjectScope: string;
subjectId: string;
score: number; // 0.6..0.85
suggestedAction: string;
openedBy: 'system:auto' | string; // userId
openedAt: string;
traceId: string;
at: string;
}
interface FraudCaseDecided {
schemaVersion: '1';
eventId: string;
caseId: string;
decision: 'CONFIRM_FRAUD' | 'DISMISS' | 'REFINE_FEATURES';
reason: string;
decidedBy: string; // userId
decidedAt: string;
actionExecuted: boolean;
traceId: string;
at: string;
}
interface FraudCaseActionDispatched {
schemaVersion: '1';
eventId: string;
caseId: string;
action: string; // e.g. SUSPEND_SENDER_ID
dispatchedSubject: string; // NATS subject the action was published on
dispatchedEventId: string; // FK to that event
dispatchedBy: string; // userId
traceId: string;
at: string;
}
2.11 fraud.tenant_score.updated.v1
interface FraudTenantScoreUpdated {
schemaVersion: '1';
eventId: string;
tenantId: string;
previousTier: 'SAFE' | 'WATCH' | 'RISKY' | 'HIGH_RISK' | 'PROBATION';
newTier: 'SAFE' | 'WATCH' | 'RISKY' | 'HIGH_RISK' | 'PROBATION';
score: number; // 0..1
contributingFactors: Array<{ category: string; weight: number; }>;
modelVersions: Record<string,string>; // category → modelVersion
computedAt: string;
traceId: string;
at: string;
}
Deduplication: events are emitted only on tier transitions, not on every recompute.
2.12 Model lifecycle: fraud.model.promoted.v1, fraud.model.rolled_back.v1, fraud.model.artifact.tamper.v1
interface FraudModelPromoted {
schemaVersion: '1';
eventId: string;
modelId: string;
previousVersion: string;
newVersion: string;
category: string;
pipeline: string;
evaluationMetrics: { auc: number; f1: number; precision: number; recall: number; brier: number; };
promotedBy: string; // userId
promotedAt: string;
traceId: string;
at: string;
}
interface FraudModelArtifactTamper {
schemaVersion: '1';
eventId: string;
modelId: string;
versionId: string;
expectedSha256: string;
observedSha256: string;
artifactUri: string;
detectedAt: string;
traceId: string;
at: string;
}
2.13 Feed lifecycle: fraud.feed.exported.v1, fraud.feed.heartbeat.v1, fraud.feed.imported.v1
interface FraudFeedExported {
schemaVersion: '1';
eventId: string;
feedId: string;
exportDate: string; // YYYY-MM-DD
format: 'MISP_2_4' | 'STIX_2_1';
outputRefs: { misp?: string; stix?: string; }; // S3 URIs
sha256: { misp?: string; stix?: string; };
signatureKeyId: string; // HSM key alias
presignedUrls?: { misp?: string; stix?: string; expiresAt: string; };
indicatorCount: number;
traceId: string;
at: string;
}
interface FraudFeedImported {
schemaVersion: '1';
eventId: string;
feedId: string;
source: string; // 'regulator-atra', 'peer-mnt', etc.
format: 'MISP_2_4' | 'STIX_2_1';
added: number;
updated: number;
expired: number;
signatureValid: true;
importedAt: string;
traceId: string;
at: string;
}
2.14 Alerts: fraud.alert.feed.signature.invalid.v1, fraud.alert.model.unavailable.v1
interface FraudAlertFeedSignatureInvalid {
schemaVersion: '1';
eventId: string;
feedId: string;
source: string;
reason: 'BAD_SIGNATURE' | 'UNKNOWN_KEY' | 'KEY_REVOKED';
expectedKeyId: string;
observedKeyId?: string;
severity: 'CRITICAL'; // PagerDuty
detectedAt: string;
traceId: string;
at: string;
}
2.15 fraud.audit.v1
Append-only mirror of every state-change in fraud.audit_log for SIEM consumption.
interface FraudAudit {
schemaVersion: '1';
eventId: string;
auditId: string;
entityType: 'CASE' | 'MODEL' | 'FEED' | 'ALLOWLIST' | 'PATTERN' | 'TENANT_SCORE';
entityId: string;
action: 'CREATE' | 'UPDATE' | 'DECIDE' | 'PROMOTE' | 'ROLLBACK' | 'IMPORT' | 'EXPORT';
actorUserId: string | null; // null = system
ip?: string;
userAgent?: string;
before?: object; // null on CREATE
after?: object; // null on DELETE
traceId: string;
occurredAt: string;
at: string;
}
3. Consumed events
| Subject | Producer | Stream | Purpose |
|---|---|---|---|
firewall.audit.v1 | sms-firewall-service | FIREWALL_EVENTS | Per-verdict evidence for AIT/SIM-box detection |
sms.events.status.v1 | sms-orchestrator | SMS_EVENTS | Outbound message lifecycle for AIT, OTP-class tagging |
sms.dlr.inbound.v1 | dlr-processor | SMS_DLR | DLR success-rate features (UC-03, UC-11) |
sms.mo.inbound.v1 | smpp-connector (MO) | SMS_MO | Inbound MO traffic for SIM-box pattern detection |
cdr.generated.v1 | cdr-mediation-service | CDR_EVENTS | Per-message CDR for grey-route arbitrage detection |
consent.revoked.v1 | consent-ledger-service | CONSENT_EVENTS | Recipient revocation for OTP-harvesting heuristic |
compliance.audit.v1 | compliance-engine | COMPLIANCE_AUDIT | Cross-correlate fraud signals with policy verdicts |
sender.id.suspended.v1 | sender-id-registry-service | SENDER_ID_EVENTS | Confirm action dispatch closed the loop |
Consumer durability: all consumers are AckExplicit with maxDeliver = 5 and a 60-s ack-wait. Failed messages route to <stream>.deadletter with reject_reason.
4. Consumer contracts (by subscribing service)
| Service | Subjects | Action |
|---|---|---|
sms-firewall-service | fraud.detected.simbox.v1, .ait_ring.v1, .greyroute.v1, .simbox_network.v1 | Promote subject to firewall.peer_quarantine or dynamic blocklist |
sender-id-registry-service | fraud.case.action_dispatched.v1 (filter action=SUSPEND_SENDER_ID) | Suspend sender-ID lifecycle |
compliance-engine | fraud.tenant_score.updated.v1, fraud.detected.otp_grinding.v1 | Update tenant tier input; apply OTP throttle |
noc-dashboard | fraud.detected.>, fraud.case.>, fraud.alert.> | SSE/WebSocket push |
regulator-portal-service | fraud.feed.exported.v1, fraud.feed.heartbeat.v1 | Mirror to regulator SFTP |
analytics-service | fraud.audit.v1, fraud.detected.> | Long-term archival + dashboards |
siem (Splunk/SigNoz) | fraud.audit.v1, fraud.alert.> | Security event correlation |
5. PII & security rules for events
- Raw SMS body content is forbidden in any fraud event payload.
- Destination MSISDNs in cross-context events use
msisdnHash = sha256(msisdn + nationalSalt). Salt is rotated annually; rotation invalidates cross-day join keys, which is intentional. - Tenant-scoped events (e.g.
fraud.detected.ait.v1withsubjectScope=TENANT) may carry rawsenderId(which is tenant-public) but never raw subscriber MSISDNs unless the consumer is also that tenant's owner. - Rule
evidencestrings (e.g. SHAP feature names) are non-sensitive feature labels; raw values are numeric. - Event payloads are signed by the producer's RS256 key (platform-wide signing) and verified opportunistically by critical consumers (firewall, sender-id-registry).
- Event bodies with PII-adjacent fields (
msisdnHash,senderId) are classified CONFIDENTIAL-INTERNAL and must not be replicated to data sinks outside the platform without Legal review.
6. Outbox pattern
All fraud state changes write an event row to fraud.outbox in the same Postgres transaction as the aggregate change. A relay publishes to NATS with retry, exponential backoff, and ack. This guarantees no emitted events without a persisted state change and no unpublished state change for more than a few seconds.
Outbox table:
CREATE TABLE fraud.outbox (
event_id UUID PRIMARY KEY,
subject TEXT NOT NULL,
payload JSONB NOT NULL,
published_at TIMESTAMPTZ,
attempts INT NOT NULL DEFAULT 0,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_outbox_unpublished ON fraud.outbox (created_at) WHERE published_at IS NULL;
7. Schema evolution
- Additive fields with non-required defaults are non-breaking within the same
schemaVersion. - Breaking changes bump to
fraud.<topic>.v2. Subjects coexist during a deprecation window (≥ 90 days); subscribers migrate offv1before removal. - Enum values are additive. Consumers MUST treat unknown enum values as
UNKNOWNand not fail. - A schema registry mirror at
schemas.ghasi.af/fraud/<topic>/<version>publishes the canonical JSON Schema.
8. Replay & reconciliation
A daily reconciliation cron compares the fraud.outbox table against the JetStream consumer offsets for the previous 24h. Any rows with published_at IS NULL AND attempts >= 5 are alerted on (PagerDuty MEDIUM) and re-tried by the operator via POST /v1/admin/fraud/outbox/{eventId}/retry. The cron also cross-checks fraud.audit_log row counts against FRAUD_AUDIT stream message counts; deltas > 10 trigger a HIGH alert.