DEV Community

Cover image for I built a no-KYC privacy directory with incident-based trust scoring (and a map of who's been sanctioned)
selfmadevidz
selfmadevidz

Posted on

I built a no-KYC privacy directory with incident-based trust scoring (and a map of who's been sanctioned)

Over the past few months I've been building dontkyc.me, a directory of services — VPNs, email, hosting, exchanges, wallets, forums — that don't require ID verification to use. It's grown into something a bit more interesting than a plain list, so I wanted to write up how the two core pieces actually work, not just announce it.

The problem I kept running into

A service's marketing page looks fine, you sign up, and only then find out it wants a passport scan — sometimes after you've already paid. "Best VPN" lists don't usually tell you that up front, because most of them are affiliate-driven and optimized for clicks, not for this specific question. So I started keeping my own notes, and that became the site.

It currently covers 400 services across 30 categories, 251 of which have a KYC level of 0 (no ID required at all). No ads, no Google Analytics, no tracking scripts. You don't need an email to register — accounts are passwordless, using a generated private key instead.

Scores that aren't just a number someone typed in

Every listing gets a privacy score, a trust score, and an explicit KYC level (0–2). The part I think is actually worth sharing: the trust score isn't a static field an admin edits whenever they feel like it. It's tied to a service_incidents table — seizures, breaches, forced policy changes, scam reports, each with a date, a source, and a score_impact value — and the current score is just where that history has landed.

That means I can reconstruct the score's trajectory over time instead of only showing a snapshot. Here's roughly how that works (simplified from the real code):

``php
// Walk backwards from the current score to find where it started,
// then forwards again to build a point for every incident.
$incidentsAsc = array_reverse($incidents); // stored DESC, we want chronological
$totalImpact = array_sum(array_column($incidentsAsc, 'score_impact'));
$runningScore = $currentTrustScore - $totalImpact;

$points = [['date' => $baselineDate, 'score' => clamp($runningScore)]];

foreach ($incidentsAsc as $incident) {
$runningScore += $incident['score_impact'];
$points[] = [
'date' => $incident['incident_date'],
'score' => clamp($runningScore),
'title' => $incident['title'],
'delta' => $incident['score_impact'],
];
}
``

No separate "history" table to keep in sync, no risk of the stored history drifting from the current score — it's derived, so it's always consistent by construction. The chart only renders when a service actually has at least one incident with a nonzero impact; otherwise it would just be a flat, meaningless line. One real example from the data: a mixer service's trust score went 9 → 5 → 4 → 4 across three documented incidents, which tells a very different story than just seeing "4/10" with no context.

Mapping where enforcement is actually happening

The second piece is the Sanctions & Restrictions Radar — a world map built from the same incident log, filtered to incidents with a country code attached. Countries are shaded by severity, and clicking one shows which listed services were affected there and why.

The part I like most is the before/after comparison: three buttons (3 / 6 / 12 months ago) render two small maps side by side — then vs. now — with a one-line summary like "Then: 2 incidents in 1 country. Today: 5 incidents in 3 countries." The "then" snapshot is just the same aggregation query with an incident_date <= ? cutoff, computed server-side and shipped with the page load, so clicking a preset doesn't need another round trip.

It's intentionally narrow in scope — only incidents tied to a listed service, each with a date and source, not a general sanctions-list dump. Coverage is naturally uneven (better where I can actually find reporting), so an empty country means "not yet documented," not "nothing happened."

The boring, honest parts

  • Stack: plain PHP + MySQL, server-rendered, no JS framework, minimal build tooling. I'd rather put the time into data quality than into tooling.
  • It's a solo project, done in my spare time. Coverage has gaps and there are definitely mistakes in there — every listing has a "suggest an edit" button, and corrections get reviewed.
  • A high score isn't an endorsement. It means the documented attributes currently hold up, not "go trust this with your money." Always DYOR.

If you're building something with a similar "derive state from an event log instead of storing a mutable snapshot" pattern, or you've solved the "two small maps, one data fetch" problem differently, I'd genuinely like to compare notes in the comments.

Top comments (0)