#136 Add fraud-signal detector that flags coordinated betting from th…FIXED#257
Merged
Conversation
…etting from the same Stellar address graph FIXED
|
@veloura-dev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
Author
|
PLEASE REVIEW |
Contributor
Author
|
@greatest0fallt1me please review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔎 Findings (gaps discovered in the codebase)
Walked the
veloura-dev/predictify-backendrepository on the default branch and confirmed the GrantFox fraud-signal feature was entirely missing:fraud_flagstable existed anywheresrc/db/schema.tshad 0 matches forfraud;drizzle/migrations/had no0011_*file (slot 0011 was free between0010_markets_fts.sqland0012_follows.sql)grep -ri "union.?find|graph" src/returned nothing relevantsrc/workers/contained onlybackupVerifier,indexer,indexerGapScan,marketResolver,webhookWorkersrc/routes/admin/only hadaudit.tsandreconciliation.tspredictionstable lacked a "funding source" field — yet the issue calls out "shared funding sources" as a primary signalsrc/db/schema.tspredictionscolumns:id, marketId, userId, outcome, amount, txHash, status, result, createdAt— no funder columndocs/ls docs/returned a small set, none relatedls tests/ | grep -i fraudreturned nothingConclusion: this was a greenfield feature, not a regression — the issue was fully unimplemented.
🛠 Fix features delivered
1. Database layer — additive migration
0011_fraud_flags.sqlALTER TABLE predictions ADD COLUMN IF NOT EXISTS funding_source text;— nullable, no default → fully backwards compatible with every existing row and every other insert path in the codebase.predictions_funding_source_idx WHERE funding_source IS NOT NULL→ cheap lookup, skips legacy nulls.fraud_flagstable with:(cluster_key, user_id)UNIQUE index → re-runs of the worker are idempotent, never duplicate findings.statusCHECK constraint (open/dismissed/confirmed) → enforced reviewer state machine.evidence jsonb→ carries the full graph context per finding.correlation_id text→ end-to-end trace from request → log → DB row.(status, created_at DESC)index → admin list is fast even at millions of rows.IF NOT EXISTS→ migration is re-runnable safely.2. Graph builder (pure function — no I/O, fully unit-testable)
fraudService.ts :: buildGraph(rows)produces undirected edges between Stellar addresses for any of:SHARED_FUNDING_SOURCESHARED_TX_HASHREPEATED_PATTERN(marketId, outcome, amount)placed inside the same 5-minute bucket — sybil signalEdges are deduplicated (
reason::detail::a::bkeyed) and self-loops are guarded.3. Clustering — classic Union-Find (DSU)
UnionFindclass with path compression + union-by-rank → O(α(n)) per op.clusterize(graph)ignores singleton components (configurableMIN_CLUSTER_SIZE = 2).key— deterministic id (addrs.sort().join("|")) so the same cluster across runs maps to the same row.members— sorted addresses.edges— the supporting evidence.score— sum of edge weights → admins can sort by severity.4. Persistence — idempotent batch upsert
DrizzleFraudRepo.upsertFlags(rows)uses Drizzle'sON CONFLICT (cluster_key, user_id) DO UPDATEto:reason,score,evidence,correlation_id,updated_aton existing ones.status,reviewed_by,reviewed_at).5. Background worker —
src/workers/fraudDetector.tsrunOnce(opts)— single scan; returns a structuredRunScanResultsummary; never throws (errors logged, next interval retries → keeps the in-process scheduler stable).start(intervalMs = 15 min, opts)— periodic timer; immediate kick-off + setInterval;unref()-ed so it doesn't block shutdown.stop()— clean teardown.correlationIdper run if none was supplied → tracing.require.main === module) for ad-hoc operator runs.6. Admin review endpoint —
src/routes/admin/fraud.tsGET /api/admin/fraud/flags?status=open&limit=50POST /api/admin/fraud/scanlookbackMs/maxPredictions)Security and hygiene features stacked on the router:
requireAdminmiddleware — JWT withrole: "admin"; 403 on any failure (no enumeration leak).statusenum,limitrange 1–200,lookbackMs≤ 7 days,maxPredictions≤ 100 000,.strict()body rejects unknown keys.{ error: { code, message, details?, requestId } }.X-Request-Idresponse header → clients can correlate with server logs.7. Observability
pino) at every public entry point:fraud_scan: start,fraud_scan: complete,fraud_detector: run complete,fraud_detector: run failed.fraud_flags.correlation_id). Verified in tests:8. Performance & safety guards
funding_sourcekeeps the lookup cheap when most rows have NULL.9. Documentation —
docs/fraud-signal.mdEvery public symbol in
fraudService.ts,fraudDetector.ts, androutes/admin/fraud.tsalso has a JSDoc block explaining responsibility and invariants.10. Tests — 42 cases across 3 suites, all passing
tests/fraudService.test.tstests/fraudDetector.test.tstests/adminFraud.test.ts🎯 Acceptance-criteria traceability
fraudService.ts :: buildGraphfraudService.ts :: UnionFind+clusterizefraud_flagstable +DrizzleFraudRepo.upsertFlagsroutes/admin/fraud.ts(GET /flags,POST /scan)requireAdmin+ rate-limit + Zod{ error: { code, message, details, requestId } }pino+correlationIdthreaded everywheredocs/fraud-signal.md+ JSDoc on every exportCLOSE #136