1F · Methodology

Risk Scoring Methodology

How 1F assigns a 0–100 risk score to an Ethereum address. Implementation: RiskScorer.cs:153 ComputeScore.

Inputs

InputSourceCardinality
Sanctions registryOFAC SDN crypto-assets list, daily refresh (SanctionsRefreshService)~600 addresses
KnownAddressesHand-curated + scraped (CoinGecko, DefiLlama, Uniswap Sybil)~19,000 labels
BridgeRegistry22 bridge contracts across 6 chainsper-chain
CEX listCurated subset of KnownAddresses (category == "cex")~56
Mixer listTornado Cash deposit/withdrawal pools, Sinbad, ChipMixer remnants~15
Graph topologyGraphIndex (static) + HotGraph (live ingestion)global

Scoring components

The total score is the sum of four components, capped at 100:


total = mixerExposure(0–40) + sanctionedProximity(0–30) + patternFlags(0–20) + addressAge(0–10)

1. Mixer exposure (0–40)


mixerRatio   = mixer-touching txs / total txs
mixerExposure = min(40, mixerRatio * 200)    # 20% mixer-tx == 40 points

Counts inbound *and* outbound. A direct deposit *or* withdrawal both count. We do not weigh deposit vs withdrawal differently — both are signal.

2. Sanctioned proximity (0–30)

We stop at 2 hops on purpose: every popular CEX hot wallet is N-hops from *something* sanctioned, so deeper traversal becomes pure noise.

3. Pattern flags (0–20)

Heuristic detectors:

Each flag adds 4–8 points; cap at 20.

4. Address age (0–10)

Younger-than-7-days addresses with non-trivial value activity score the full 10. The premise: brand-new, freshly funded addresses interacting with high-value flows are statistically over-represented in fraud cases. This component decays to 0 by ~90 days.

Special cases

CaseScoreWhy
Address on OFAC SDN list100, CRITICALSanctions are binary
MEV searcher (category == "mev")15Active but legitimate; flagged for visibility, not blocked
Bridge contract10Bridges are not actors
CEX deposit address5Custodial, KYC'd at withdrawal
Address not in graph15, LOWLimited history → mild caution

Risk levels


score >= 80 → CRITICAL
score >= 60 → HIGH
score >= 40 → MEDIUM
score >= 20 → LOW
score <  20 → MINIMAL

Exposure profile math

When a caller asks for the *exposure profile* of an address (used by /api/forensics/cex-exposure, /api/forensics/mixer-correlate), we don't compute a single score — we return per-counterparty-class buckets:


{
  sanctionedExposure: { count, totalValueWei },
  mixerExposure:      { count, totalValueWei, byMixer: {...} },
  cexExposure:        { count, totalValueWei, byExchange: {...} },
  bridgeExposure:     { count, totalValueWei, byBridge:   {...} },
}

totalValueWei is summed from edge-level realValueWei (same field used for the heatmap and PageRank tie-breaks). For ERC-20 transfers this is the wei-equivalent at our normalization (see BlockIngestionService.NormalizeTokenValue); pre-receipts ingestion this can be off for fee-on-transfer tokens.

Caveats — read these

Endpoint surface


POST /api/forensics/screen          # bulk batch screen, ≤1000 addrs
GET  /api/forensics/cex-exposure/{address}
GET  /api/forensics/mixer-correlate/{address}

For the underlying score per address, callers can hit the existing /api/risk-score/{address} endpoint which returns the full breakdown (component scores, flagged reasons).