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.