Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/fraud-signal.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ clusters suspicious addresses with **Union-Find**, and persists findings to
* **Admin endpoints** — `src/routes/admin/fraud.ts`:
* `GET /api/admin/fraud/flags?status=open&limit=50` — paginated review
* `POST /api/admin/fraud/scan` — manual trigger (admin only)
* `PATCH /api/admin/fraud/flags/:id` — review transitions (`open→confirmed|dismissed`, `confirmed→dismissed` with note; `dismissed` is terminal). Invalid transitions return `409` with code `invalid_transition`; missing/required-note validation return `400` with code `validation_error`. Every transition creates an `audit_logs` entry with action `fraud_flag.review` (note stored in audit metadata only).Concurrent updates use optimistic status checks.

All endpoints require an admin JWT (`role: "admin"`) and are rate-limited
per-token (60 req/min by default).
Expand All @@ -52,16 +53,42 @@ Migration `drizzle/migrations/0011_fraud_flags.sql`:
* Creates `fraud_flags` with `(cluster_key, user_id)` unique index,
status enum, evidence JSONB, score, and reviewer audit columns.

Migration `drizzle/migrations/0031_fraud_flags_carried_from.sql`:

* Adds `fraud_flags.carried_from` UUID FK → `fraud_flags(id)` (ON DELETE SET NULL)
to track when a re-detected finding inherits a prior dismissal.

## Carry-forward behavior

When `runFraudScan` detects a finding for a user who already has the most
recent `dismissed` flag, it compares the new evidence's reason set against
the prior dismissal's reasons. If the new reasons are a subset of the prior
reasons (no new signal type appeared), the new flag is written as
`status='dismissed'` with `carried_from` set to the prior dismissal's id. If
any new reason appears, the finding is reopened as `status='open'`. Cluster
keys are SHA-256 of sorted member addresses (`|`-joined) for new scans.

## Worker

`src/workers/fraudDetector.ts` supports:
* `node dist/workers/fraudDetector.js` for one-off scans (exits on completion)
* `FRAUD_DETECTOR_INTERVAL_MS=<ms>` for recurring runs with SIGINT/SIGTERM
graceful shutdown (timer stopped, DB pool closed with timeout)
* `npm run fraud-detector` / `npm run fraud-detector:dev` scripts

## Operational notes

* Correlation IDs flow from the request (or are generated per scan) into
every log line and every persisted `fraud_flags.correlation_id`.
every log line and every persisted `fraud_flags.correlation_id`. Routes
echo `x-correlation-id` and `x-request-id` on responses.
* The worker never throws — failures are logged and the next interval
retries. This keeps the in-process scheduler stable.
* `MIN_CLUSTER_SIZE = 2` keeps noise low. Tune via the constant in
`fraudService.ts` if needed.
* Default lookback is **24 h**, capped at 10 000 predictions per scan to
protect memory on large datasets.
* `DrizzleFraudRepo` transition updates never touch `status/reviewedBy` via
`ON CONFLICT DO UPDATE` (idempotent re-scan / audit integrity).

## Testing

Expand Down
1 change: 1 addition & 0 deletions drizzle/migrations/0031_fraud_flags_carried_from.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "fraud_flags" ADD COLUMN IF NOT EXISTS "carried_from" uuid REFERENCES "fraud_flags"("id") ON DELETE SET NULL;
Loading