Skip to main content

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

StreamSubjectsRetentionReplicasDedup window
FRAUD_EVENTSfraud.detected.>90 days32 min
FRAUD_CASESfraud.case.>13 months (regulatory)32 min
FRAUD_TENANT_SCOREfraud.tenant_score.>365 days32 min
FRAUD_MODELfraud.model.>365 days32 min
FRAUD_FEEDfraud.feed.>365 days3
FRAUD_ALERTfraud.alert.>90 days32 min
FRAUD_AUDITfraud.audit.v113 months (regulatory)32 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

SubjectProducerStreamPurpose
firewall.audit.v1sms-firewall-serviceFIREWALL_EVENTSPer-verdict evidence for AIT/SIM-box detection
sms.events.status.v1sms-orchestratorSMS_EVENTSOutbound message lifecycle for AIT, OTP-class tagging
sms.dlr.inbound.v1dlr-processorSMS_DLRDLR success-rate features (UC-03, UC-11)
sms.mo.inbound.v1smpp-connector (MO)SMS_MOInbound MO traffic for SIM-box pattern detection
cdr.generated.v1cdr-mediation-serviceCDR_EVENTSPer-message CDR for grey-route arbitrage detection
consent.revoked.v1consent-ledger-serviceCONSENT_EVENTSRecipient revocation for OTP-harvesting heuristic
compliance.audit.v1compliance-engineCOMPLIANCE_AUDITCross-correlate fraud signals with policy verdicts
sender.id.suspended.v1sender-id-registry-serviceSENDER_ID_EVENTSConfirm 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)

ServiceSubjectsAction
sms-firewall-servicefraud.detected.simbox.v1, .ait_ring.v1, .greyroute.v1, .simbox_network.v1Promote subject to firewall.peer_quarantine or dynamic blocklist
sender-id-registry-servicefraud.case.action_dispatched.v1 (filter action=SUSPEND_SENDER_ID)Suspend sender-ID lifecycle
compliance-enginefraud.tenant_score.updated.v1, fraud.detected.otp_grinding.v1Update tenant tier input; apply OTP throttle
noc-dashboardfraud.detected.>, fraud.case.>, fraud.alert.>SSE/WebSocket push
regulator-portal-servicefraud.feed.exported.v1, fraud.feed.heartbeat.v1Mirror to regulator SFTP
analytics-servicefraud.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.v1 with subjectScope=TENANT) may carry raw senderId (which is tenant-public) but never raw subscriber MSISDNs unless the consumer is also that tenant's owner.
  • Rule evidence strings (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 off v1 before removal.
  • Enum values are additive. Consumers MUST treat unknown enum values as UNKNOWN and 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.