Skip to content

Repository files navigation

Orbweaver

Coordinated promotion-abuse rings are invisible order by order. This finds them in the graph — and prices what it costs to be wrong.

testslive consoledocslicensepythonlast commit

Live console · Docs site · All results · What broke · Issues

Orbweaver: 0.7292 ring precision against a 0.2242 base rate, at 0.371 real customers per fraudster caught, 44% of rings with a case open the night before

In one minute

A group running many accounts through one delivery-app promotion looks fine order by order; the fraud only exists in the connections between the accounts, which a detector scoring one transaction at a time cannot see. Pruning to suspicious accounts, then peeling for dense structure, catches them at 0.7292 ring precision against a base rate of 0.2242 — 3.252× chance — at a measured cost of 0.371 real customers swept into a ring for every fraudster it catches.

The finding I would defend hardest: dense is not the same as fraudulent, and it replicates on every unrelated dataset I have tried it on. Unpruned, the same extractor lands below chance here (0.31×) and at exactly zero on YelpChi - 25 rings, 1,914 accounts, 0 of them fraudulent. Pruned first, the identical code reaches 14.3× on Amazon reviewers and 6.9× on YelpChi reviews - three platforms, two of them nothing like promotion abuse, saying the same thing (why this matters).

Live console · Full results · What broke


ContentsOverview · What it is · Features · Stack · Architecture · Flow · Pipeline · Results · Caveats · ML boundary · Failures · In production · Deployment · Structure · Start · API · Tests · Roadmap · Research


Overview

The problem. A delivery app gives ₹100 off your first order. A group runs fifty accounts between them and takes ₹5,000. Every order looks normal on its own — the fraud only exists in the connections: a shared address, a shared device, one UPI ID paying for all of them.

Why nothing on the shelf catches it. Thirdwatch scores "the probability of the order being fraudulent" — per order. Vulcan runs a transformer over ~3,000 signals — per transaction. A ring is invisible to both, because a ring is not a property of any order. I checked rather than assumed: as of 2 September 2026, every Sprint 2026 fraud launch is scoped to a transaction, a chargeback or an identity. Nothing graph-level exists.

The approach. Score accounts once, prune to the suspicious region, then let a densest-subgraph algorithm with a proved bound decide ring membership — so "why is this account in this ring?" is checkable arithmetic, not a model's opinion. Each ring ships as a case file with evidence, rupees at stake, and the cost of being wrong.

Keywords:fraud-detectiongraph-algorithmsdensest-subgraphanomaly-detectionpaymentsfintechxgboostpythonreproducible-research

What this is

A detection pipeline, not a model. The only learned component scores accounts; a deterministic densest-subgraph peeling with a proved ½-approximation bound decides who is in a ring. A ring is not a property of any single transaction — which is why per-transaction scoring cannot see one, and why the number quoted throughout is ring precision rather than accuracy or AUC.

what decides membershipmeasuredvs chance
Structure alone — peel the raw graph, no score cut-off0.0696 ring precision0.31×
The model alone — held-out accounts0.3796 AUPRC1.693×
Score, prune, then peel0.7292 ring precision3.252×

Order is what does the work. Peeled first, the densest subgraphs are large ordinary communities — people who happened to use the same promotion — and the queue lands below chance.

Key Features

FeatureWhat it does
Multi-relation graphEdges weighted by entity rarity × measured fraud lift, fitted on training accounts only
Prune, then peelScore cut-off first, then greedy peeling with a ½-approximation bound
Evidence per ringShared entities, coverage, platform-wide rarity, day concentration, ₹ at stake
Cost always attachedEvery precision number ships with real customers swept in per fraudster caught
Capacity-aware policyExact knapsack over reviewer minutes → review / auto-hold / ignore, with both numbers
Anchored nightly replayStable case ids so tonight's ring is recognisably tomorrow's case
Live analyst consoleFastAPI + HTMX: queue, case files, offers, account lookup, replay. No build step
Reproduciblemake reproduce regenerates every number, table and figure. Nothing typed by hand
Honest failure log36 dated entries — what broke, what I believed, why it was wrong

Tech Stack

LayerTechnology
LanguagePython 3.11+ (developed on 3.13)
Graph & algorithmspython-igraph, NumPy · greedy peeling + Fast Belief Propagation, hand-written
MLXGBoost (isotonic-calibrated) · GraphSAGE (reported alternative) · scikit-learn
Datapandas, PyArrow/Parquet, pydantic-validated YAML config
ConsoleFastAPI + HTMX, server-rendered — no npm, no bundler
ReportingMatplotlib; Markdown + HTML generated by eval/report.py
Testing / CIpytest · GitHub Actions on every push
HostingVercel (console) · GitHub Pages (docs & evidence)

System Architecture

flowchart TB
subgraph INGEST[" 1 · INGEST "]
direction LR
O(["orders<br/><small>43.9M rows</small>"]) --> G["multi-relation graph<br/><b>rarity x relation weight</b>"]
end
subgraph DETECT[" 2 · DETECT "]
direction LR
S["account scorer<br/><b>XGBoost, 39 features</b><br/>the one learned step"] --> P["prune<br/><b>score cut-off tau</b>"]
P --> PE["peel<br/><b>densest subgraph</b><br/>proved 1/2 bound"]
end
subgraph ACT[" 3 · ACT "]
direction LR
R["ring + evidence<br/>shares, rarity, INR, cost"] --> PO["policy<br/><b>review / hold / ignore</b>"]
PO --> Q(["analyst queue<br/>case files"])
end
INGEST --> DETECT --> ACT
PE -. anchored around fixed accounts,<br/>so case ids survive the night .-> NIGHTLY
subgraph NIGHTLY[" replayed one night at a time "]
direction LR
N1["night 1<br/><small>chance</small>"] --> N2["night 2"] --> N3["night 3"] --> N4["night 4<br/><small>headline</small>"]
end
classDef learned fill:#fff2ec,stroke:#c2410c,stroke-width:2px,color:#1c1c1c
classDef proved fill:#f0fdf4,stroke:#166534,stroke-width:2px,color:#1c1c1c
classDef plain fill:#ffffff,stroke:#c9c9c9,color:#1c1c1c
classDef terminal fill:#f5f5f4,stroke:#6b6b6b,color:#1c1c1c
class S learned
class PE proved
class G,P,R,PO plain
class O,Q terminal
class N1,N2,N3,N4 plain
Loading
  1. Build the graph — accounts linked by shared entities, each edge weighted by how rare that entity is and how much that sharing predicts fraud.
  2. Score accounts — gradient boosting over 39 engineered features. The one learned step in the pipeline.
  3. Prune, then peel — the cut-off removes ordinary accounts; densest-subgraph decides membership on what remains.
  4. Attach evidence — what they share, how rare, when they acted, rupees at stake, cost of being wrong.
  5. Price the response — capacity-aware policy recommends an action under a stated reviewer budget.

→ Depth: docs/architecture.md · docs/design-decisions.md

Application Flow

sequenceDiagram
autonumber
actor A as Analyst
participant C as Console (FastAPI+HTMX)
participant I as In-memory index
participant R as Run artefacts
A->>C: Open review queue
C->>R: read ring_report.json
R-->>C: rings · evidence · ₹ at stake
C-->>A: queue sorted by money at stake
A->>C: click a ring
C-->>A: case file + recommended action
A->>C: GET /check/{account}
C->>I: array lookup (built once at import)
I-->>C: score · neighbours · ring membership
C-->>A: answer + measured latency (sub-ms)
Loading

Data & ML Pipeline

flowchart LR
D1[(PPA<br/>4 GB)] --> C1[validate<br/>+ cap entities]
C1 --> F1[39 features<br/>train window only]
F1 --> T1[XGBoost<br/>+ isotonic]
T1 --> E1{{evaluate}}
D2[(Amazon · YelpChi<br/>IEEE-CIS)] -.transfer check.-> E1
E1 --> M1[ring precision<br/>+ FP cost]
style E1 fill:#fff2ec,stroke:#c2410c
Loading
StageWhat happensWhy it is done this way
SourcesPPA (only public labelled promotion-abuse ring dataset) + Amazon, YelpChi, IEEE-CIS as transfer checksOne dataset proves nothing about a method. Why this data →
CleaningSchema validation; entity capping via N_max. No relabelling, no augmentation, no dropped outliersA labelled evaluation is only worth something if the labels were set by someone with no stake in the result
FeaturesGraph edges as alpha_r / log(2 + users(e)); 39 account featuresalpha_r is measured fraud–fraud lift per relation, fitted on training accounts only
TrainingXGBoost + isotonic calibration. Strictly temporal split — week 1 trains, week 2 testsRandom node splits leak. tests/test_split_no_leak.py fails the build if one appears
AlternativesGraphSAGE (mini-batch, CPU-deterministic) and Fast Belief Propagation, run at every label budgetReported beside XGBoost whichever way they fall — and one of them wins
EvaluationRing precision vs base rate · FP cost · held-out AUPRC · hostel test · adversarial fragmentationPrecision alone is not a deployable number

Measured file-by-file findings about the raw release: docs/data.md.

Results

Regenerated by make reproduce — never typed by hand. Full detail with all 16 figures: docs/results.md.

Graph35,701,750 edges over the accounts active in the scoring window
Ring precision0.7292 against a base rate of 0.2242 — 3.252×
Cost of that0.371 real customers placed in a ring per fraudster caught
Without the score cut-off0.0696 — 0.31×, i.e. worse than picking at random
Account scorerAUPRC 0.3796 on held-out accounts, 1.693× random
Three relations I cannot rebuildworth +0.122 precision and +269 fraud accounts on the authors' own graph
Hostel test2 of 2,446 legitimate co-located groups touched (0.08%)
The relation only a platform can seeworth +0.024 to +0.038 ring precision at equal review capacity (250-500 accounts)
Time to detection, replaying night by nightmedian 4 of 4 nights; 33.4% of a ring's spend still ahead of it when it is found
Ranking rings by confidencethe mean member score wins at 200 rings (0.6739) — a trained ring model gets 0.5989, density 0.5814
Yesterday's rings as a feature+0.0011 AUPRC — it reaches 0.15% of held-out accounts. /check answers in 0.01 ms at the median
Behaviour edges against fragmentation+0.0237 ring precision when the ring is split into threes, -0.0023 when it is split into twenties
The same method on a payment processor's graph0.5079 precision, 18.138× its base rate, at 0.969 good cards per fraudulent one caught
What one analyst an hour a night stops₹67,900 of promotion value against ₹200 for working the queue in order, for ₹16,040 of legitimate value harmed (assumed rupees)
Telling a crowd from a ring by when it formedburst-weighted ring precision -0.0149 on PPA; on IEEE-CIS the apartment-cluster weakness is unchanged at 4 of 7 touched at every resolution tried
Which offers are being farmedtop 50 offers by size (325,494 accounts) cover 7.1% of all labelled fraud, 19.7x the 0.36% ring recall ceiling
How many confirmed cases before this worksprune-then-peel first beats the base rate at 1,146 confirmed accounts (0.5% of the training pool)
Spreading what few labels there areFast Belief Propagation, no fitted model: 0.4615 held-out AUPRC at full labels, 0.9886 ring precision pruning on its beliefs alone
A ring you can find again tomorrow44% of final rings had a case open the night before (global peeling: 4%); 0.7167 precision against 0.7292 for the cost of a case id

Where the base rate comes from. 68,533 of the 305,617 accounts carrying a label at all are fraud — 68,533 / 305,617 = 0.2242. Unlabelled accounts are left out of the denominator rather than counted as normal, because ring precision counts only labelled members and the two have to be measured the same way. The other convention is reported beside this one in docs/results.md.

The thirteen investigations — including the four that did not work

#FindingOutcome
1Dense is not the same as fraudulent. On the raw graph the densest subgraphs are large ordinary communities — people who happened to use the same promotion — and ring precision comes out below the base rate. Filtering to suspicious accounts first, then looking for dense structure inside that region, is what makes the output useful. It replicates on two unrelated datasets: run unchanged on Amazon reviewers and YelpChi reviews, the unpruned extractor again lands below base rate — on YelpChi at exactly zero, 25 rings and 1,914 accounts without a single fraudster — while pruning first reaches 14.3× and 6.9×Held up — the one I would defend hardest
2The relation that dominates the graph carries the weakest signal. Promotion edges are 70% of the graph at 1.76× fraud lift; location edges are 16% at 3.71×. Weighting relations by measured evidential value, fitted on training accounts only, follows directlyHeld up — drives the edge weighting
3The link only a platform can see is worth measuring, not assuming. On YelpChi, removing the one relation that spans businesses costs two to four points of ring precision at equal review capacity. I had argued the aggregator's advantage with simulated edges before this; now it is a measurement on real labels, and the simulated version is only a sensitivity checkHeld up — a measurement replaced a guess
4One night of data is worth nothing, and rings do not survive the night. Replaying a night at a time, a single night puts the queue at chance, and it takes four nights to reach the headline. Worse, no ring found on the last night had a recognisable predecessor — a case could not be tracked at all. Anchoring the extraction fixed it: 44% of final rings now have a case open the night before against 0% for the global extractor, at a cost of 0.0125 precisionFailed, then fixed by anchoring
5The crudest baseline beat the model I built to replace it. A ring-level confidence model lost to simply ranking by the mean score of a ring's members, at every depth. The reason is visible in the training data: 90.6% of candidate rings are already fraudulent, so there is almost nothing for a ring-level model to separateNegative result
6Ring history does not transfer to the next window. Feeding "was this account in a ring last window" back into the account score moves held-out AUPRC by +0.0011. The ceiling was set before the model ran: the feature is non-zero for 0.15% of held-out accounts. Rings are window-specific objects — the accounts recur, the groupings do notNegative result
7Behaviour edges raise the price of fragmentation without defeating it. Splitting a ring into cells of three takes precision from 0.73 to 0.45. Mutual nearest-neighbour edges in behaviour space — which an attacker cannot cut by severing shared entities — recover +0.0237 of that, and nothing at all at cells of twentyPartial — fragmentation still works
8The method transfers to a payment processor's graph, and its weakest point moves with it. On IEEE-CIS the same pipeline reaches 0.5079 ring precision at 18.1× base rate. But the apartment-building analogue of the hostel test touches 4 of 7 clusters, against 2 of 2,446 here, because the billing address is at once the most informative relation and the thing that legitimately ties every card in a building togetherTransfers, at a cost
9The analyst is not the bottleneck I built for. Pricing review against auto-hold under a fixed budget, the fraud stopped does not change between thirty analyst-minutes a night and two hundred and forty — auto-holding already stops it. What analyst time buys is a 39% fall in legitimate value harmed. On these assumptions the reviewer is a false-positive control rather than a detectorHeld up — not what I expected
10Telling a crowd from a ring by when it formed did not pay off, and the fair test explains why. Burstiness weighting costs 0.0149 precision on PPA with no clean per-relation story. IEEE-CIS was built to test this properly, with second-resolution timestamps PPA cannot offer, and returned as clean a null as this project has produced: the same 4 of 7 apartment clusters at every resolution from one hour to one day. The billing address is not informative despite being shared by a legitimate building — it is informative because of itNegative result — a clean null
11Which offers are farmed splits into a precision ranking and a coverage ranking — and they are not the same offers. A leakage score built from no label beats base rate by 2.5–3.8× at top 25 and 50. But leakage ranks small, concentrated offers first, capping how much fraud it can touch: fifty offers by leakage cover 0.04% of labelled fraud, against 7.1% — 19.7× the ring's own recall ceiling — ranked by raw size, at the cost of reviewing 325,494 accounts rather than a few hundredHeld up — a capacity decision, not a technical one
12A team with almost no confirmed labels is not starting from nothing. Prune-then-peel already beats base rate at the smallest fraction tested — 1,146 confirmed accounts, 0.5% of the training pool. Held-out AUPRC keeps climbing all the way to 100%; no plateau appears in the range testedHeld up
13A method with no fitted model at all beat both learned scorers, and not for the reason I expected. Fast Belief Propagation — one sparse linear system, a proven convergence condition, priors from confirmed labels only — reaches 0.4615 held-out AUPRC against XGBoost's 0.3796 and GraphSAGE's 0.3819, and pruning on its beliefs alone lifts ring precision to 0.9886 with three times the recall at the same review cost. The hypothesis was that propagation wins when labels are scarce; the data says the opposite — it trails both learned scorers from 0.5% through 20% of the pool and only crosses over at 50%. Propagation needs seeds to spread fromHeld up — hypothesis wrong, result right

Every one of these is written up in full, with the figure that produced it, in docs/results.md.

What These Numbers Do Not Prove

LimitDetail
Not Indian dataPPA is Chinese food delivery. The method is data-agnostic; the numbers are not. The hostel test is the closest proxy — real validation needs Indian data
Transfer runs are weakerAmazon/YelpChi ship no timestamps (account-disjoint, not forward in time) and do ship node features PPA lacks — their higher numbers say more about the datasets
The release ≠ the paperTest week only: 3,267,961 accounts, 10,012,449 edges — not 5.69M/29M. Three of eight relations are empty in the order files. Measured here
₹ rests on assumptionsPPA ships no monetary amounts. The ₹ columns rank options against each other; they mean nothing absolute
Ring recall is low by designRings surface a few hundred accounts for review, not the population. A queue asks what share of what it looks at is worth looking at
Baselines aren't like-for-likePublished baselines have no account holdout and count unlabelled as negative. Both conventions are reported

Where Machine Learning Is Used, and Where It Is Not

StageLearned?
Graph construction, edge weightingNo — arithmetic over measured per-relation lift
Account scoringYes — XGBoost, the only learned component
Ring extractionNo — deterministic peeling with a proved bound
Evidence, ₹ at stake, FP costNo — counting and stated arithmetic
The decision to actNo — a human reads the case file

A model scores accounts. It does not decide who is in a ring. No language model sits anywhere in the detection or decision path. → docs/design-decisions.md

What Broke

FAILURES.md — 36 dated entries, the five that mattered linked at the top. The one I'd point at: I trained a model on 183,370 accounts that did not exist, because the two order files are independently re-indexed and I joined week-1 behaviour to week-2 labels through an id that means nothing across the boundary. It trained cleanly. It converged. All 27 features had a fraud/normal ratio of exactly 1.000 — that's what gave it away.

Running this inside a payment stack

What this needs is not features but shared entities: one table of (account, entity_type, entity_id), the order history the account features are built from, and whatever cases have already been confirmed. An aggregator holds every entity type that matters — the device, the billing address, the card fingerprint, the e-mail domains on both sides, the browser.

It runs as two loops: a nightly batch that builds, scores, prunes and peels, and an online lookup that answers for one account. The scorer is the swappable part — orbweaver/scoring/xgb_graph.py keeps the extraction scorer-agnostic, so an existing transaction risk score can replace XGBoost and the ring layer sits on top of a risk stack rather than competing with it.

Labels needed to start1,146 confirmed accounts — 0.5% of the training pool — already beats the base rate
Online path/check answers in 0.01 ms at the median and 0.059 ms at p95 — a read of the nightly pass, not a graph computation
Nightly pathanchored extraction, so 44% of tonight's rings are a case that was already open yesterday rather than a fresh queue every morning
On a card processor's graphthe same pipeline, unchanged: 0.5079 ring precision at 18.138× the base rate, at 0.969 good cards flagged per fraudulent one
What one merchant cannot seethe platform arm surfaces 681 accounts at 0.9956 precision; drop the relation that spans businesses and the merchant arm surfaces 1,856 at 0.9488

What it does not solve.

  • The address relation is the risk. It is at once the most informative edge and the thing that legitimately ties a building together. On delivery data the hostel test touches 2 of 2,446 legitimate co-located groups; on card data its apartment analogue touches 4 of 7. That test has to be rerun on whatever data this meets next.
  • An adaptive attacker degrades it. Splitting every ring into cells of 3 takes ring precision from 0.7292 to 0.4539.
  • It is a queue, not a net. Ring recall is 0.0036 by construction — this surfaces a few hundred accounts worth reviewing, not the population.

Deployment & Infrastructure

ConcernHow it works
ConsoleVercel, serverless Python. api/index.py re-exports the FastAPI app; vercel.json installs requirements-demo.txt (6 packages) and ships demo/**
DocsGitHub Pages from docs/ with .nojekyll. Every page there is generated by make report
CIGitHub Actions runs the data-free suite on every push to main (badge above)
DeploysBoth surfaces redeploy on push to main. No manual step
Demo modeConsole serves the committed demo/ bundle whenever data/processed/ is empty — a clone with no dataset still runs. /health reports mode, rings served, bundle size
DeterminismFixed seeds in config/default.yaml; the GNN trains on CPU because Metal's scatter reductions are non-deterministic

Project Structure

Orbweaver/
├── orbweaver/ # the package
│ ├── data/ # loaders: PPA, GADBench, IEEE-CIS · subsampling · lockstep
│ ├── features/ # 39 account features · ring context
│ ├── scoring/ # XGBoost · GraphSAGE · sampler · Fast Belief Propagation
│ ├── rings/ # peeling · anchored extraction · ring scorer · review policy
│ ├── adversarial/ # fragmentation · duplication · behavioural twins
│ └── console/ # FastAPI + HTMX console, demo-bundle mode
├── eval/ # one investigation per script, one JSON artefact each
├── tests/ # temporal-split + planted-ring tests gate every number
├── config/default.yaml # every threshold, seed and cost assumption
├── scripts/ # dataset download · demo smoke test · prose voice check
├── docs/ # generated results, figures, published site
├── demo/ # the ≤2 MB bundle console and Pages run from
└── api/ # Vercel entrypoint
DocRead it if…
docs/results.mdYou want every number and all 16 figures
docs/why-this-data.mdYou're asking "why this dataset, and is it trustworthy"
docs/architecture.mdYou want the five stages in depth
docs/design-decisions.mdYou want the ML boundary, and why it sits there
docs/data.mdYou want the raw release measured file by file
docs/threat-model.mdYou want what it catches, misses, and how to evade it
FAILURES.mdYou want the honest log. Start here
ETHICS.mdYou want the scope boundary in six lines

Getting Started

Option A — the console, no dataset needed (about a minute):

git clone https://github.com/adarshcod30/Orbweaver.git
cd Orbweaver
pip install -r requirements-demo.txt
make console # → http://127.0.0.1:8000

It detects that data/processed/ is empty and serves the committed demo/ bundle: real rings, real evidence, real numbers from a full run.

Option B — reproduce everything from raw data:

make setup # full dependencies
make download # PPA from OSF, ~4 GB, resumable + md5-verified
make reproduce # every stage, end to end

make reproduce-core runs the pipeline alone in ~50 minutes. Every stage is its own target (make graph, make score, make rings, make policy, make replay) writing JSON into data/processed/.

Usage & API Reference

MethodRouteReturns
GET/Review queue, filterable by shared relation and known-fraud count
GET/ring/{rank}One ring's case file — members, shared entities, rarity, ₹ at stake
GET/offersWhich promotions are farmed, ranked by a leakage score that uses no label
GET/replayThe window replayed night by night — precision, persistence, ₹ stopped
GET/findingsEvery figure with a plain-language caption
GET/check/{account}JSON: everything known about one account + measured latency
GET/healthMode (demo/full), rings served, bundle size
curl -s https://orbweaver-adarshcod30s-projects.vercel.app/check/291571 | python3 -m json.tool

Testing

make test# the suite
make check # the suite + the prose check this repo is held to

The tests that matter aren't the unit tests: tests/test_split_no_leak.py fails the build if any week-2 account appears in training, and the planted-ring tests assert the extractor recovers a ring deliberately inserted into a synthetic graph — so a silent regression in the peeling objective can't pass.

Roadmap

  • Validate on Indian payments data — the one thing the hostel test can't substitute for
  • A shared payment-instrument relation across merchants — the edge no single platform can build
  • Streaming extraction instead of nightly batch, so a case opens the hour it forms
  • Reviewer feedback as a label source, closing the loop between queue and scorer
  • Seller collusion and refund rings — same algorithm, different labels (threat model)

Research Foundation

WorkWhat it contributed here
PromoGuardian / PPA, IEEE S&P 2026 · arXiv · dataThe labelled dataset everything is measured against. No code or checkpoint reused
Charikar, APPROX 2000The ½-approximation the peeling objective relies on
Hooi et al., FRAUDAR, KDD 2016Camouflage-resistant weighting; the bound for the node-prior objective
Bahmani, Kumar & Vassilvitskii, VLDB 2012Batch peeling — how rings get extracted at 35.7M-edge scale
Khuller & Saha, ICALP 2009The NP-hardness result motivating approximation over exact search
Xu, Ma, Fang et al., SIGMOD 2023The empirical bound cited for greedy peeling under a size ceiling
Dai et al., Anchored Densest Subgraph, SIGMOD 2022The anchored formulation that makes a case survive the night
Greene, Doyle & Cunningham, ASONAM 2010Life-cycle events and the Jaccard threshold for case identity
Koutra et al., ECML-PKDD 2011Fast Belief Propagation, implemented as specified — and it beat the learned scorer
Beutel et al., CopyCatch, WWW 2013The lockstep-in-time argument the burstiness arm tests
Tang et al., GADBench, NeurIPS 2023Why this starts with gradient boosting; two of the transfer datasets
Dou et al., CARE-GNN, CIKM 2020The Amazon and YelpChi releases used unchanged as a transfer check
Razorpay, ThirdwatchThe per-order framing this complements rather than replaces

Contributing

Personal research project — not looking for feature contributions, but corrections are genuinely welcome, especially a number you cannot reproduce. Open an issue with the command you ran and what you got. Building on it: fork → branch → make check → PR describing which numbers moved.

License & Ethics

MIT — LICENSE. Detection only. ETHICS.md sets the boundary in six lines: no attack tooling, public research data only, case files rather than automated verdicts, false-positive cost next to every detection number, simulated edges always labelled simulated, and shared attributes as evidence for a human to weigh — never as guilt.

Cite with CITATION.cff.

Contact

Adarsh Dwivedi23ucs509@lnmiit.ac.in · GitHub

Live console · Docs · Results · Failures

Built for the Razorpay AI Buildathon, Track 02.

About

Finding coordinated promotion-abuse rings in transaction graphs, and reporting what it costs to be wrong about them. Densest-subgraph ring extraction over a rarity-weighted account graph, with a capacity-aware review policy and a nightly replay that survives the night.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages