<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: selfmadevidz</title>
    <description>The latest articles on DEV Community by selfmadevidz (@selfmadevidz_01a1a2b9876e).</description>
    <link>https://dev.to/selfmadevidz_01a1a2b9876e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158421%2F79b8f7ba-040a-4c2e-ba72-00668c32c965.png</url>
      <title>DEV Community: selfmadevidz</title>
      <link>https://dev.to/selfmadevidz_01a1a2b9876e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/selfmadevidz_01a1a2b9876e"/>
    <language>en</language>
    <item>
      <title>I built a no-KYC privacy directory with incident-based trust scoring (and a map of who's been sanctioned)</title>
      <dc:creator>selfmadevidz</dc:creator>
      <pubDate>Fri, 02 Oct 2026 19:45:23 +0000</pubDate>
      <link>https://dev.to/selfmadevidz_01a1a2b9876e/i-built-a-no-kyc-privacy-directory-with-incident-based-trust-scoring-and-a-map-of-whos-been-4c7j</link>
      <guid>https://dev.to/selfmadevidz_01a1a2b9876e/i-built-a-no-kyc-privacy-directory-with-incident-based-trust-scoring-and-a-map-of-whos-been-4c7j</guid>
      <description>&lt;p&gt;Over the past few months I've been building &lt;a href="https://dontkyc.me" rel="noopener noreferrer"&gt;dontkyc.me&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It currently covers &lt;strong&gt;400 services across 30 categories&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scores that aren't just a number someone typed in
&lt;/h2&gt;

&lt;p&gt;Every listing gets a &lt;strong&gt;privacy score&lt;/strong&gt;, a &lt;strong&gt;trust score&lt;/strong&gt;, and an explicit &lt;strong&gt;KYC level&lt;/strong&gt; (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 &lt;code&gt;service_incidents&lt;/code&gt; table — seizures, breaches, forced policy changes, scam reports, each with a date, a source, and a &lt;code&gt;score_impact&lt;/code&gt; value — and the current score is just where that history has landed.&lt;/p&gt;

&lt;p&gt;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):&lt;/p&gt;

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

&lt;p&gt;$points = [['date' =&amp;gt; $baselineDate, 'score' =&amp;gt; clamp($runningScore)]];&lt;/p&gt;

&lt;p&gt;foreach ($incidentsAsc as $incident) {&lt;br&gt;
    $runningScore += $incident['score_impact'];&lt;br&gt;
    $points[] = [&lt;br&gt;
        'date'  =&amp;gt; $incident['incident_date'],&lt;br&gt;
        'score' =&amp;gt; clamp($runningScore),&lt;br&gt;
        'title' =&amp;gt; $incident['title'],&lt;br&gt;
        'delta' =&amp;gt; $incident['score_impact'],&lt;br&gt;
    ];&lt;br&gt;
}&lt;br&gt;
``&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping where enforcement is actually happening
&lt;/h2&gt;

&lt;p&gt;The second piece is the &lt;a href="https://dontkyc.me/sanktionsradar" rel="noopener noreferrer"&gt;Sanctions &amp;amp; Restrictions Radar&lt;/a&gt; — 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;incident_date &amp;lt;= ?&lt;/code&gt; cutoff, computed server-side and shipped with the page load, so clicking a preset doesn't need another round trip.&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring, honest parts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack&lt;/strong&gt;: plain PHP + MySQL, server-rendered, no JS framework, minimal build tooling. I'd rather put the time into data quality than into tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a solo project&lt;/strong&gt;, 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A high score isn't an endorsement.&lt;/strong&gt; It means the documented attributes currently hold up, not "go trust this with your money." Always DYOR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>security</category>
      <category>php</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
