<?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: Ilia Ivanov</title>
    <description>The latest articles on DEV Community by Ilia Ivanov (@phaz).</description>
    <link>https://dev.to/phaz</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%2F4182062%2F9aeea96b-ed9c-4e8d-afa1-9872c7a10ca4.png</url>
      <title>DEV Community: Ilia Ivanov</title>
      <link>https://dev.to/phaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phaz"/>
    <language>en</language>
    <item>
      <title>I built a site chatbot that isn't allowed to make things up</title>
      <dc:creator>Ilia Ivanov</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:07:45 +0000</pubDate>
      <link>https://dev.to/phaz/i-built-a-site-chatbot-that-isnt-allowed-to-make-things-up-1fk4</link>
      <guid>https://dev.to/phaz/i-built-a-site-chatbot-that-isnt-allowed-to-make-things-up-1fk4</guid>
      <description>&lt;p&gt;In 2024 a tribunal in Canada made Air Canada honor what its website chatbot had told a customer about refunds. The airline argued the bot was responsible for its own words. The tribunal said the company is responsible for everything on its site, chatbot included.&lt;/p&gt;

&lt;p&gt;For a small business that is the real problem with site chatbots. A wrong opening time or an invented price costs more than the bot saves. So I built a demo where the model is only allowed to draft, and plain code decides what the customer sees: &lt;strong&gt;&lt;a href="https://phazww.github.io/front-desk/" rel="noopener noreferrer"&gt;Front Desk&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The demo runs on Juniper Street Bakery, a bakery I made up. Its whole "site" is 13 short entries: hours, prices, cakes, allergens, delivery, payment. The assistant knows nothing else.&lt;/p&gt;

&lt;p&gt;The model is Gemma 4 E2B, running in the browser through WebLLM at temperature 0. Every question goes through four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find.&lt;/strong&gt; Code picks up to three entries that match the question. If nothing matches, the model is not asked at all and the customer gets the phone number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft.&lt;/strong&gt; The model reads only those entries and returns JSON: the answer, the id of the entry it used, and the sentence it took the answer from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check.&lt;/strong&gt; The quoted sentence has to be in that entry. Every number in the answer has to be in it too ("4 pm" counts as 16:00, "$4" does not count as $4.50). And every word that carries meaning has to appear somewhere in what the model was given.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show or step back.&lt;/strong&gt; If the first sentence fails a check, the draft is thrown away and the customer sees the entry's exact text or the contact details. Later sentences that fail are cut.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the first runs showed
&lt;/h2&gt;

&lt;p&gt;I wrote 24 test questions with known answers, 6 of them about things the site never mentions (pizza, a loyalty card, Christmas hours).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 1: 17 of 24.&lt;/strong&gt; No invented fact reached the customer, but most misses had the same cause. Asked "Do you deliver bread?", the model set &lt;code&gt;answerable: false&lt;/code&gt;, because to it "no" felt like "not answerable". Rewording that rule fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 2: 21 of 24&lt;/strong&gt;, and a real hole. Asked "Do you have a loyalty card?", it answered "No, we do not have a loyalty card." The site says nothing about loyalty cards. There were no numbers in that sentence, so my number check let it through.&lt;/p&gt;

&lt;p&gt;That is where the word check came from: "loyalty" is not in anything the model was given, so the sentence is not backed by the site. The same check later cut "That is our capacity." from an otherwise correct answer about seats.&lt;/p&gt;

&lt;h2&gt;
  
  
  On questions I hadn't tuned on
&lt;/h2&gt;

&lt;p&gt;After writing the checks I added 7 new questions and ran all 31 live. It got 28 right, 5 of the 7 new ones. One miss was my own bug: the model wrote "$8.00" and the site says "$8", and the number check treated them as different. With that fixed, the same recorded drafts pass 29 of 31.&lt;/p&gt;

&lt;p&gt;The two misses left are worth showing, because the checks can't catch them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Is the bakery open on Christmas Day?" got the regular opening hours. True, every word on the site, but not an answer.&lt;/li&gt;
&lt;li&gt;"Is the cinnamon roll vegan?" got a correct "no", but pointed at the pastry prices instead of the vegan entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A wrong yes or no where every word is on the site would pass the same way. That is why I think a bot like this needs a test set of the business's real questions before it goes live, not only a good prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do small businesses even have chatbots?
&lt;/h2&gt;

&lt;p&gt;I took 1,020 business websites in Austin from OpenStreetMap (cafes, restaurants, dentists, clinics, salons, bakeries), dropped chains that share one domain, and fetched each homepage once. 592 loaded. 27 had any chat widget at all. 15 of those were Wix's built-in chat with the owner. The only ones I could identify as AI assistants were on chain sites.&lt;/p&gt;

&lt;p&gt;So the chatbot market is crowded, but the local cafe mostly has no assistant at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  A widget without a model
&lt;/h2&gt;

&lt;p&gt;That led to a second mode. The &lt;a href="https://phazww.github.io/front-desk/widget.html" rel="noopener noreferrer"&gt;widget&lt;/a&gt; is one script tag. Without a model endpoint it runs in quote mode: it finds the entry that matches and shows it word for word, or gives the phone number. It cannot make anything up and costs nothing per question. On the 31 test questions it shows the right entry or declines 29 times, though I also used those questions while choosing search words, so expect less on a new site.&lt;/p&gt;

&lt;p&gt;With an endpoint added, it drafts answers and runs the same checks, falling back to quote mode when one fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo with recorded answers, no download: &lt;a href="https://phazww.github.io/front-desk/#recorded" rel="noopener noreferrer"&gt;https://phazww.github.io/front-desk/#recorded&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live model and the test set: &lt;a href="https://phazww.github.io/front-desk/#tests" rel="noopener noreferrer"&gt;https://phazww.github.io/front-desk/#tests&lt;/a&gt; (needs WebGPU, downloads the model once)&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/phazww/front-desk" rel="noopener noreferrer"&gt;https://github.com/phazww/front-desk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I set this up for small businesses on their own content, with a test set of their customers' questions. Cafes and bakeries first: &lt;a href="https://phazww.github.io/front-desk/cafes/" rel="noopener noreferrer"&gt;https://phazww.github.io/front-desk/cafes/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What would you check that I'm not checking?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Email verifiers tell you an address exists. I built one that tells you if you may write to it</title>
      <dc:creator>Ilia Ivanov</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:24:10 +0000</pubDate>
      <link>https://dev.to/phaz/email-verifiers-tell-you-an-address-exists-i-built-one-that-tells-you-if-you-may-write-to-it-2fb3</link>
      <guid>https://dev.to/phaz/email-verifiers-tell-you-an-address-exists-i-built-one-that-tells-you-if-you-may-write-to-it-2fb3</guid>
      <description>&lt;p&gt;Apify Store has a lot of email verifiers. The biggest ones I found had between 400 and 1,400 users in the last 30 days, at about $0.90 per thousand addresses. They answer one question: does this mailbox exist?&lt;/p&gt;

&lt;p&gt;None of them answered the question I kept running into: am I allowed to send a first marketing email to it?&lt;/p&gt;

&lt;p&gt;That answer depends on the country of the person you are writing to. Germany, Spain and Italy want consent before the first email, even to a company address. The US allows it, but wants a postal address and an opt-out in every message. Sweden protects people and not companies. Slovakia and Slovenia care whether the company published the address itself.&lt;/p&gt;

&lt;p&gt;I had already read those laws for 31 countries for a &lt;a href="https://phazww.github.io/can-i-cold-email/" rel="noopener noreferrer"&gt;free web checker&lt;/a&gt;. This week I turned the same rules into an Apify Actor: &lt;strong&gt;&lt;a href="https://apify.com/phaz/cold-email-law-checker" rel="noopener noreferrer"&gt;Cold Email Law Checker&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You give it a lead list. It adds a verdict to every row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;law_verdict&lt;/code&gt;: &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;cond&lt;/code&gt; (only in specific cases), &lt;code&gt;consent&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;law_reason&lt;/code&gt;: one sentence on why&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;law_mustInclude&lt;/code&gt;: what the email itself has to contain, such as an opt-out or a postal address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;law_lawSource&lt;/code&gt;: a link to the law or the regulator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;law_checked&lt;/code&gt;: the date I checked that country&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your own columns stay, so you can filter the dataset and keep only the rows you may email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to feed it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The output of another Actor run.&lt;/strong&gt; Pick the dataset of your lead scraper. Nested fields like &lt;code&gt;location.country&lt;/code&gt; are found automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A CSV link.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A JSON list&lt;/strong&gt; of addresses or objects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works out which field is the email, which is the country and which says where the address came from. You can name them yourself if it guesses wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two details that change the verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use the person's country, not the company's.&lt;/strong&gt; The rules follow the recipient. A German employee of a US company is a German recipient. When a row has both, the checker prefers the person's location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say where the address came from.&lt;/strong&gt; In Canada, Australia, the Netherlands, Slovakia and Slovenia the answer depends on whether the company published the address itself. Add a &lt;code&gt;source&lt;/code&gt; column: &lt;code&gt;published&lt;/code&gt;, or &lt;code&gt;vendor&lt;/code&gt; for anything a tool found or guessed. Values like &lt;code&gt;apollo&lt;/code&gt; or &lt;code&gt;hunter&lt;/code&gt; count as vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;The Actor and the web checker share one rules file, generated from the same data, so they cannot drift apart. The tests run an example export through the Actor and compare every verdict with what the website says for the same rows. Running the example input before launch caught a bug, and now there is a test for it: the Actor looked for the &lt;code&gt;source&lt;/code&gt; column only in the first row, so a Slovak address published on the company's own site got the wrong verdict whenever the first lead had no source.&lt;/p&gt;

&lt;p&gt;On my laptop it checks 20,000 leads in about four and a half seconds. On Apify the first runs took one to two seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;This is a research summary, not legal advice. It covers the first unsolicited marketing email to a business recipient. GDPR applies on top whenever an address identifies a person. Every verdict links to its source so you can check it, and if you know one of these laws better than I do, open an issue on &lt;a href="https://github.com/phazww/cold-email-law-checker" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and I will fix it.&lt;/p&gt;

&lt;p&gt;It is free while I find out whether people use it.&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>email</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I read the cold email law of 30 countries. Writing to info@ needs consent in 10 of them</title>
      <dc:creator>Ilia Ivanov</dc:creator>
      <pubDate>Wed, 14 Oct 2026 11:00:11 +0000</pubDate>
      <link>https://dev.to/phaz/i-read-the-cold-email-law-of-30-countries-writing-to-info-needs-consent-in-10-of-them-4jf6</link>
      <guid>https://dev.to/phaz/i-read-the-cold-email-law-of-30-countries-writing-to-info-needs-consent-in-10-of-them-4jf6</guid>
      <description>&lt;p&gt;Last week I almost sent cold emails to small shops in Spain. Then I read the Spanish law and found out a first marketing email there needs consent, even when you write to a company. Vendor blogs said different things, so I went to the source: the law text or the regulator's own guidance, one country at a time.&lt;/p&gt;

&lt;p&gt;I am at 30 countries now. Here is what they say about the first unsolicited marketing email to a business. This is a research summary, not legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing to a generic company address (info@company)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No consent needed (14):&lt;/strong&gt; United States, United Kingdom, France, Belgium, Ireland, Sweden, Norway, Finland, Portugal, Estonia, Latvia, Luxembourg, Slovenia, Croatia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allowed with conditions (5):&lt;/strong&gt; Canada, Australia, Netherlands, Slovakia, Bulgaria.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consent first (10):&lt;/strong&gt; Germany, Spain, Italy, Denmark, Austria, Poland, Switzerland, Czech Republic, Malta, Greece.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not verified yet (1):&lt;/strong&gt; Cyprus. The consent rule there covers only natural persons, and companies fall under a separate 2005 order I have not been able to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing to a named person (jane.doe@company)
&lt;/h2&gt;

&lt;p&gt;This is where it gets strict. Plainly allowed without consent in only 4: the US, the UK, France (if the offer relates to the person's job) and Sweden. In several countries the law protects "natural persons" and says nothing clear about an employee's work address, so for 7 countries I marked it as not verified instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that surprised me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Belgium&lt;/strong&gt; treats info@ and jane@ differently. A royal decree lifts the consent rule for legal persons only when the address is impersonal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slovakia&lt;/strong&gt; has an unusual exception: no consent is needed when you write to contact details that a company or sole trader has published itself (section 116(15) of the Electronic Communications Act).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slovenia&lt;/strong&gt; says almost the same thing about named people: you can use a person's address if the company published it as its own contact address (article 226(6) of ZEKom-2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulgaria&lt;/strong&gt; keeps an official opt-out register for companies. You can email a company unless its address is in the register, and the email has to be recognisable as unsolicited as soon as it arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Greece and Malta&lt;/strong&gt; protect companies exactly like individuals. Consent first, for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The US&lt;/strong&gt; is the opposite: CAN-SPAM is an opt-out law and covers B2B email, but every email needs a postal address and a working way to opt out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What every country asks for, even where consent is not needed
&lt;/h2&gt;

&lt;p&gt;Say who you are, and give a working way to stop. Most of the countries above put that in the law text itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Every country has its own page with the verdict for each kind of address, what the email must contain, and links to the law or the regulator. Each page shows the date I checked it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://phazww.github.io/can-i-cold-email/" rel="noopener noreferrer"&gt;https://phazww.github.io/can-i-cold-email/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you know one of these laws better than I do and see a mistake, tell me in the comments and I will fix it and credit you. The next countries I want to add are Lithuania, Hungary and Romania. Their official law portals were not reachable from where I am, so if you have a link to the current text, that helps too.&lt;/p&gt;

</description>
      <category>email</category>
      <category>marketing</category>
      <category>startup</category>
      <category>law</category>
    </item>
    <item>
      <title>Cold email law changes at every border, so I put an open model in the browser to check my drafts</title>
      <dc:creator>Ilia Ivanov</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:40:01 +0000</pubDate>
      <link>https://dev.to/phaz/cold-email-law-changes-at-every-border-so-i-put-an-open-model-in-the-browser-to-check-my-drafts-2h4p</link>
      <guid>https://dev.to/phaz/cold-email-law-changes-at-every-border-so-i-put-an-open-model-in-the-browser-to-check-my-drafts-2h4p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I'm 18, I live in Shenzhen and I do freelance web work. Last week I started sending cold emails to small online shops in Europe. Then I read the Spanish law and found out that a first marketing email there needs consent, even when you write to a company. I stopped and went to read the rest.&lt;/p&gt;

&lt;p&gt;I read the rules for 20 countries, one law at a time, and I am up to 28 now. They do not agree with each other. Germany, Spain and Italy want consent first. France lets you write to a professional if the offer relates to their job. The US wants a postal address and a way to opt out in every email. Sweden protects people but not companies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coldproof&lt;/strong&gt; checks a cold email draft against the law of the country it is going to.&lt;/p&gt;

&lt;p&gt;You pick the recipient's country and the kind of address, and paste your draft. You get three things back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The legal verdict for that country, with a link to the law or the regulator.&lt;/li&gt;
&lt;li&gt;A checklist of what that country requires inside the email, with the exact words the model found in your draft for each item.&lt;/li&gt;
&lt;li&gt;A fixed version with the missing lines added, in the language of your email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Who would pay for it: sales teams and outbound agencies that send to several countries at once. One wrong campaign can mean a fine or a burned domain, and nobody on the team has time to read 28 laws. A single draft is free. The page has a "for teams" offer at $19 per seat with a button that only counts interest, because I wanted to know if anyone cares before I build billing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://phazww.github.io/coldproof/" rel="noopener noreferrer"&gt;https://phazww.github.io/coldproof/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fotyliha6n0ghpytoftzs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fotyliha6n0ghpytoftzs.jpg" alt="Coldproof result for the sample draft: 1 of 5 required elements found, and the fixed version with the missing lines added" width="800" height="978"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a phone, or if you do not want to download a model, press "See a recorded example". It replays a saved answer from Gemma on the sample draft through the same checks, and it says so on the page.&lt;/p&gt;

&lt;p&gt;On a computer, click "Try a sample" and then "Check my draft". The first run downloads the model once. After that a check takes about 10 seconds on my Mac with Gemma, and about 6 with the smaller Qwen model.&lt;/p&gt;

&lt;p&gt;It needs WebGPU, so use a recent Chrome, Edge or Safari on a computer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/phazww" rel="noopener noreferrer"&gt;
        phazww
      &lt;/a&gt; / &lt;a href="https://github.com/phazww/coldproof" rel="noopener noreferrer"&gt;
        coldproof
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Check a cold email against the law of the recipient's country. An open-weight model runs in your browser, the draft never leaves your machine.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Coldproof&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Check a cold email draft against the law of the recipient's country. An open-weight model runs in your browser through WebGPU, reads the draft and reports what it contains, quoting the exact words. The page compares that with checked rules for 28 countries and returns the missing pieces and a fixed version. The draft never leaves your machine.&lt;/p&gt;
&lt;p&gt;Live: &lt;a href="https://phazww.github.io/coldproof/" rel="nofollow noopener noreferrer"&gt;https://phazww.github.io/coldproof/&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it is built&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rules.js&lt;/code&gt;: per-country rules for the first unsolicited marketing email to a business recipient, each with its source. The research comes from my earlier project &lt;a href="https://github.com/phazww/can-i-cold-email" rel="noopener noreferrer"&gt;can-i-cold-email&lt;/a&gt;, checked against the law or the regulator on 10 and 11 October 2026.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app.js&lt;/code&gt;: loads the model with &lt;a href="https://github.com/mlc-ai/web-llm" rel="noopener noreferrer"&gt;WebLLM&lt;/a&gt;, asks it for a JSON report with quotes, verifies that every quote is really in the draft, and assembles the fixed version from hand-written wording in 13 languages.&lt;/li&gt;
&lt;li&gt;Two open-weight models to choose from: Gemma 4 E2B…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/phazww/coldproof" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;No server, no API key, no build step. Three files: the rules, the page, and the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The default model is &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt;, running locally in the browser through &lt;a href="https://github.com/mlc-ai/web-llm" rel="noopener noreferrer"&gt;WebLLM&lt;/a&gt; on WebGPU. There is a second option, &lt;strong&gt;Qwen 3.5 2B&lt;/strong&gt;, which is about 1 GB and quicker to get. Both are open-weight.&lt;/p&gt;

&lt;p&gt;The first version let the model do everything: read the draft, decide what was wrong, rewrite it. It looked great on the English sample. Then I tested a French email. The model was asked to add an opt-out line and wrote, in polite French, "if you would like to receive information about our services, please reply to this message". That is the opposite of an opt-out. For a tool about the law, that is the worst possible bug, because it looks correct.&lt;/p&gt;

&lt;p&gt;So I took jobs away from the model until it had only the one it is good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model reads.&lt;/strong&gt; It gets the draft and returns a JSON report: the sender's name, the sender's company, whether there is a way to say stop, a postal address, a plain commercial offer, and what language the email is in. For every "yes" it must quote the exact words. WebLLM's JSON schema mode keeps the output in shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The page checks the quotes.&lt;/strong&gt; If the model says "yes, there is an opt-out" but the quoted words are not in the draft, the page counts it as missing and says so. The quoted sentence also has to look like an opt-out. A small model cannot talk its way past this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The law is not the model's job.&lt;/strong&gt; Which elements each country requires comes from a rules file. I built that research last weekend for another project of mine, &lt;a href="https://github.com/phazww/can-i-cold-email" rel="noopener noreferrer"&gt;can-i-cold-email&lt;/a&gt;, and checked each country against the law text or the regulator. The app in this post was built this week on top of that data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The legal lines are fixed wording.&lt;/strong&gt; The opt-out sentence and the other added lines exist in 13 languages and are fixed in advance, not generated when you run a check. The model only tells the page which language the email is in. I do not speak all 13, so corrections are welcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The subject check is plain code.&lt;/strong&gt; A first email that starts with "Re:" is pretending to be a reply. That does not need a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent countries get a stop sign.&lt;/strong&gt; If the country requires consent before the first email, the page says so and does not offer a rewrite. No wording fixes that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I wrote a proper test set: &lt;a href="https://github.com/phazww/coldproof/blob/main/TESTS.md" rel="noopener noreferrer"&gt;30 drafts in 13 languages&lt;/a&gt;, each with the answer a careful human would give. Some are traps: "unsubscribe" used in another sense, a signature with only a first name, "The Invoicely team" with no person, a city with no street address, a vague question with no offer, an opt-out hidden in a P.S.&lt;/p&gt;

&lt;p&gt;Early on, the smaller Qwen model accepted the signature "Sam" as identifying the sender, with no company anywhere, and once pointed to "We make switching free." as the opt-out. So the sender check now asks for the name and the company separately, and the opt-out quote has to contain opt-out wording.&lt;/p&gt;

&lt;p&gt;The first full run gave Gemma 28 of 30. Both misses were mine. Gemma found the opt-out in "just reply 'no thanks'" and "let me know and I won't follow up", quoted the right words, and my own second check threw the answer away because it did not know those phrasings. I widened the wording list, checked it still rejects sentences like "We make switching free.", and ran everything again.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Drafts fully right&lt;/th&gt;
&lt;th&gt;Seconds per draft&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B (default)&lt;/td&gt;
&lt;td&gt;30 / 30&lt;/td&gt;
&lt;td&gt;about 10.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3.5 2B&lt;/td&gt;
&lt;td&gt;26 / 30&lt;/td&gt;
&lt;td&gt;about 7.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen still misses two opt-outs phrased as "let me know", calls a vague question a sales pitch, and calls one French pitch not commercial. That is why Gemma is the default. Anyone can rerun the set in their own browser at &lt;a href="https://phazww.github.io/coldproof/#tests" rel="noopener noreferrer"&gt;phazww.github.io/coldproof/#tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I worked with an AI coding agent the whole way. The bug with the French sentence was found by testing real drafts in the browser, not by reading the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A cold email draft is a list of things a company does not want to leak: who they are targeting, what they are offering, sometimes a customer's name. A lead list is personal data. The whole point of this tool is to help people respect privacy law. Sending that data to someone else's API to check it would be a strange way to do that.&lt;/p&gt;

&lt;p&gt;An open-weight model lets the check run on the user's own graphics card. Nothing is uploaded. There is no account, no key and no server for me to pay for, which matters when you are 18 and your hosting budget is zero. It also means the tool keeps working if I disappear.&lt;/p&gt;

&lt;p&gt;And open weights let anyone see exactly which model made the call, swap it for a better one, and test it on their own language. A closed API would have hidden the French bug behind a nicer average.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Overall&lt;/li&gt;
&lt;li&gt;Best Use of Gemma: Gemma 4 E2B is the default model and runs locally in the browser. It got all 30 test drafts in 13 languages right.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
