<?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: Dhiraj Kavuri</title>
    <description>The latest articles on DEV Community by Dhiraj Kavuri (@kinnuworks).</description>
    <link>https://dev.to/kinnuworks</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%2F4182540%2F2cb95bd5-66fd-475a-8175-e5526068aa51.png</url>
      <title>DEV Community: Dhiraj Kavuri</title>
      <link>https://dev.to/kinnuworks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kinnuworks"/>
    <language>en</language>
    <item>
      <title>A "monthly" app plan cost me $300 to leave. Now a local Gemma reads the terms before I click "Start free trial"</title>
      <dc:creator>Dhiraj Kavuri</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:52:19 +0000</pubDate>
      <link>https://dev.to/kinnuworks/a-monthly-app-plan-cost-me-300-to-leave-now-a-local-gemma-reads-the-terms-before-i-click-start-c4d</link>
      <guid>https://dev.to/kinnuworks/a-monthly-app-plan-cost-me-300-to-leave-now-a-local-gemma-reads-the-terms-before-i-click-start-c4d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F87pvbn2pfq6tqnob6lkv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F87pvbn2pfq6tqnob6lkv.gif" alt="Catch pinning a card next to a Start free trial button, then writing the exit plan" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I signed up for an app's free trial, and it turned into a monthly plan. A few weeks later I tried to cancel and found out I couldn't for three months, even though it was billed monthly. Leaving early cost $300. The rule was in the terms I had scrolled past on the way to the "Start free trial" button.&lt;/p&gt;

&lt;p&gt;I'm not going to start reading terms of service. So I built &lt;strong&gt;Catch&lt;/strong&gt;, a Chrome extension that reads them for me at that button.&lt;/p&gt;

&lt;p&gt;When a sign-up or checkout page has a price and a button, Catch reads the text around the button and the terms page it links to. Gemma 4 E2B, running in Chrome on my own GPU, double-checks the sentences that look like trouble, and Catch pins a card next to the button:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exit fee:&lt;/strong&gt; "Leaving early costs $300."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock-in:&lt;/strong&gt; "You're committed for 3 months, even though it's billed monthly."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trial turns paid:&lt;/strong&gt; "The trial turns into a paid plan at $29.99."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every line quotes the sentence it came from. Click it and the page scrolls to that sentence, or the terms page opens with it highlighted.&lt;/p&gt;

&lt;p&gt;If I press the button anyway, Catch writes my exit plan: the last day to cancel for free, the day I can leave without the fee, and how cancelling has to be done, as events in my calendar. Then I can stop thinking about it.&lt;/p&gt;

&lt;p&gt;It works on real terms too. On Xfinity's subscriber agreement it pulls out "If your Services are subject to a minimum term agreement, and you terminate… before the end of the minimum term, you may be charged an early termination fee." That's the kind of sentence I would never have scrolled to.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3MkDbNKn8Ng" width="710" height="399"&gt;
  &lt;/iframe&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%2F8zohya8efvf28m6yrzbw.png" 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%2F8zohya8efvf28m6yrzbw.png" alt="The exit plan Catch writes when you press the button anyway" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it without installing anything:&lt;/strong&gt; &lt;a href="https://kinnuworks.github.io/catch/#try" rel="noopener noreferrer"&gt;https://kinnuworks.github.io/catch/#try&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a made-up sign-up page with the same trap I fell for, and the extension's own code runs inside the page. Hover the button, read the card, press "Start free trial" for the exit plan. The demo starts with the rules, which work instantly; switch to Gemma 4 E2B to watch the model double-check them (a 3.4 GB download, once). There's also a box where you can paste the terms from any real sign-up page.&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/kinnuworks" rel="noopener noreferrer"&gt;
        kinnuworks
      &lt;/a&gt; / &lt;a href="https://github.com/kinnuworks/catch" rel="noopener noreferrer"&gt;
        catch
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A Chrome extension where a local Gemma reads the terms at the sign-up button and writes your exit plan.
    &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;Catch&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Catch is a Chrome extension that reads the terms on a sign-up page at the moment you're about to press "Start free trial". It pins a card next to the button with the catches that cost money: renewals, lock-ins, exit fees, cancellation rules and refunds. Each line quotes the sentence it came from. If you press the button anyway, Catch writes your exit plan, with the last day to cancel for free, as calendar events.&lt;/p&gt;
&lt;p&gt;Gemma 4 E2B runs in your browser on your own GPU. The page text never leaves your laptop.&lt;/p&gt;
&lt;p&gt;I built it after an app's free trial turned into a "monthly" plan I couldn't cancel for three months. Leaving early cost $300. The rule was in the terms I scrolled past.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo, no install:&lt;/strong&gt; &lt;a href="https://kinnuworks.github.io/catch/" rel="nofollow noopener noreferrer"&gt;https://kinnuworks.github.io/catch/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Video (70 s):&lt;/strong&gt; &lt;a href="https://youtu.be/3MkDbNKn8Ng" rel="nofollow noopener noreferrer"&gt;https://youtu.be/3MkDbNKn8Ng&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;sign-up page ──► find the button ──► read the text around it&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kinnuworks/catch" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The repo has the extension, the landing page, 32 tests, and the whole evaluation: the scripts that fetched the terms, the reviewed labels, and every reader's raw output, including each probability Gemma gave.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Code reads, Gemma checks
&lt;/h3&gt;

&lt;p&gt;The obvious version asks a model to read the terms and say what's wrong with them. I trust that version least. A model that writes the warning can also write a warning that isn't in the document.&lt;/p&gt;

&lt;p&gt;So Catch splits the job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code finds sentences.&lt;/strong&gt; A splitter that doesn't break on "$29.99", "e.g." or "section 4.2" cuts the terms into sentences and remembers where each one sits on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules cast a wide net.&lt;/strong&gt; Fifteen patterns pick sentences that might be about renewals, trials, minimum terms, exit fees, cancellation rules or refunds, and guess which kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma answers yes or no.&lt;/strong&gt; For each guess it gets one narrow question, such as "Does this sentence say the customer must pay a fee, or the remaining payments, if they cancel early?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code writes the card.&lt;/strong&gt; The line on the card is built from amounts and durations that code pulled out of the quoted sentence. Gemma can't turn $300 into $30, and it can't add a clause that isn't on the page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rules alone are fast and catch real things, but they cry wolf. On Peloton's terms they flag "30 days notice, in writing", which turns out to be about rejecting changes to the arbitration clause. On 1Password's, "within thirty (30) calendar days… in writing" is about erasing data after you leave. Gemma 4 E2B gave those two a 0.44 and a 0.29 chance of being a real cancellation rule, so they stay off the card.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picking the right Gemma size
&lt;/h3&gt;

&lt;p&gt;I started with Gemma 3 1B because it's an 800 MB download. I asked it eight questions where I knew the answer and looked at how likely it thought "yes" was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sentence&lt;/th&gt;
&lt;th&gt;Should be&lt;/th&gt;
&lt;th&gt;Gemma 3 1B&lt;/th&gt;
&lt;th&gt;Gemma 4 E2B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"All amounts paid are non-refundable."&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"You may cancel at any time and receive a prorated refund…"&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"…Spotify shall have no liability to you." (asked: lock-in?)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"…terminate before the end of the term, you will be charged an early termination fee."&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We will provide at least 30 days' notice prior to any new terms…"&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Subscriptions automatically renew until cancelled…"&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Credits are non-refundable, non-transferable…" (asked: lock-in?)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"…Babbel will provide you with a pro rata refund…" (asked: exit fee?)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Out of the box, at 4-bit, with my zero-shot prompt, the 1B said yes to almost everything, so on top of the rules it added nothing. Adding worked "no" examples to the prompt didn't change it. Gemma 4 E2B separated the cases without any fine-tuning, so that's what ships. A 1B fine-tuned on these labels might well do this job; that's the next experiment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trusting the confidence, not the word
&lt;/h3&gt;

&lt;p&gt;Even with the bigger model, I don't take the single word it writes. Catch runs one forward pass, looks at the model's scores for "yes" and "no" as the next word, and turns them into a probability. That gives a number between 0 and 1 for every question, so the cut-off is a choice I can measure instead of a guess. Tuned for the best balance on half of my labelled services and tested on the other half, it lands between 0.05 and 0.23. For the card I use a stricter 0.5: it shows fewer catches, but most of what it shows is real, and a card that cries wolf teaches you to ignore it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dates are arithmetic, not AI
&lt;/h3&gt;

&lt;p&gt;The exit plan never asks the model for a date. Code finds "7-day free trial", "three (3) months" and "at least 14 days before your next billing date", even when they sit in different sentences, and does calendar maths: three months from January 31 is April 30, not "90 days later". The calendar file opens in Apple Calendar, Google Calendar and Outlook, with a reminder two days before each deadline.&lt;/p&gt;

&lt;h3&gt;
  
  
  27 real terms documents, labelled by hand
&lt;/h3&gt;

&lt;p&gt;I fetched the terms of 50 subscription services; 27 could be read without running their JavaScript. Catch picked up to 8 candidate sentences from each, 136 in total. An AI assistant (not Gemma, and not the rules) suggested a label for each, and I went through all 136 one by one in a review page before any reader's output was scored. I didn't change any of them, which makes the labels only as good as my review, so every file is in the repo to check. The labels, the suggestions and the review page are all in the repo.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;th&gt;Time per document&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rules only&lt;/td&gt;
&lt;td&gt;63%&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;under 1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B, threshold tuned per fold&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;0.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B, asked about every kind (0.5)&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;1.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemma 4 E2B, rules decide lock-ins (0.5, what ships)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;85%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;78%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Precision: of the catches shown, how many were real. Recall: of the real catches among these sentences, how many were shown.&lt;/p&gt;

&lt;p&gt;The rules alone flag almost every real catch, but more than a third of what they show is wrong. With Gemma 4 E2B checking them, false alarms drop from 37% of the card to 15%, and the card keeps nearly three quarters of the real catches. The 1B made no difference at all, which matches the probe table above.&lt;/p&gt;

&lt;p&gt;The first Gemma 4 run taught me where it's weak: it rejected all nine real lock-in sentences, such as "payment obligations are non-cancelable" in Zoom's terms. It reads "minimum term" language as neutral. So lock-ins now skip the model and come straight from the rules, which is how the demo card can still show my three-month lock-in, and fine-tuning for lock-ins is the next job. Gemma also accepted Babbel's fee for skipping a booked class as an exit fee (0.92), a fair reading of the words but not what Catch is for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problems I hit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model wouldn't stay cached.&lt;/strong&gt; Gemma 4 E2B is two weight files of 1.5 and 1.6 GB. In one browser profile with a 3.9 GB storage quota, the browser silently refused to cache them, so every reload downloaded them again. The extension now asks for &lt;code&gt;unlimitedStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome kept unloading the model.&lt;/strong&gt; Extension service workers are stopped when idle, which dropped the model from the GPU every few minutes. It now lives in an offscreen document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"twenty-four months" meant four.&lt;/strong&gt; A reviewer found that my number parser skipped "twenty" and read "four". Compound numbers, European prices like "29,99 €", and a daylight-saving bug that moved a deadline by a day all have regression tests now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exit plan vanished on real checkouts.&lt;/strong&gt; The first version attached its click listener after the model finished, and a real "Start free trial" navigates away. Now it listens from the start, saves the plan the moment you press, and shows it again on the next page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where it's still wrong
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It only checks what the rules flag.&lt;/strong&gt; All 136 test sentences were picked by the rules, so the recall numbers above mean "recall among sentences the rules found". A trap written in words my fifteen patterns don't expect is invisible to Catch and to this test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The test runs the same weights in a different runtime.&lt;/strong&gt; The numbers come from Gemma 4 E2B at 4-bit through Ollama on my laptop; the extension runs the 4-bit ONNX build of the same model in the browser. Small differences between the two are possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit fees were rare in my sample.&lt;/strong&gt; Only two of the 136 sentences were real exit fees, so that row says little on its own. The exit-fee trap I fell for is in the demo, not in the test set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English only, text only.&lt;/strong&gt; Terms inside images, PDFs or a payment provider's iframe are skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not legal advice.&lt;/strong&gt; It points at the sentences worth reading yourself.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;It reads every sign-up page I visit.&lt;/strong&gt; A version of Catch that sent page text to a hosted API would be a log of everything I almost bought. With open weights running in the browser through WebGPU, the page text never leaves the laptop. The only requests are the one-time model download and the site's own terms page, fetched without cookies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could take the model apart.&lt;/strong&gt; The yes-bias only showed up because I could read Gemma's next-token scores directly, and calibrating a threshold on them is what made the 2B model usable. With the weights and the runtime in my hands, I can measure, swap models, or fine-tune.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; There's no per-page bill, so there's no reason for Catch to have a subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed model would be better:&lt;/strong&gt; coverage. A large hosted model reading the whole document would probably catch traps written in words my fifteen rules never look for. Catch only checks what the rules flag, and I'd rather miss a strangely worded clause than send my browsing to a server.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 E2B runs in Chrome through Transformers.js and WebGPU. Catch reads its yes/no probabilities from the logits and calibrates the threshold on hand-labelled data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs:&lt;/strong&gt; the demo video's narration is my own voice, cloned with ElevenLabs and generated line by line from the script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I built this with help from an AI coding assistant. Every number in this post comes from the evaluation files in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>48,975 Hugging Face models say apache-2.0. Their family trees say otherwise.</title>
      <dc:creator>Dhiraj Kavuri</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:05:57 +0000</pubDate>
      <link>https://dev.to/kinnuworks/48975-hugging-face-models-say-apache-20-their-family-trees-say-otherwise-50bb</link>
      <guid>https://dev.to/kinnuworks/48975-hugging-face-models-say-apache-20-their-family-trees-say-otherwise-50bb</guid>
      <description>&lt;p&gt;A Hugging Face model page shows you one hop of history: the license the uploader typed, and maybe the base model they fine-tuned. The terms that actually travel with a model come from every model above it. If you fine-tune a fine-tune of Llama 3.2, the Llama 3.2 agreement comes along, whatever your card says.&lt;/p&gt;

&lt;p&gt;I wanted to know how often the label on a model disagrees with the terms it inherited. Answering that for one model is a few API calls. Answering it for the whole Hub is a graph problem, so I put the whole Hub in a graph.&lt;/p&gt;

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

&lt;p&gt;The public &lt;code&gt;cfahlgren1/hub-stats&lt;/code&gt; dataset has a daily snapshot of every model and dataset on the Hub. On October 18 2026 that was 3,161,594 models. I parsed it with DuckDB and loaded it into FalkorDB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every model as a &lt;code&gt;Model&lt;/code&gt; node, with its license, task, size and downloads;&lt;/li&gt;
&lt;li&gt;927,431 lineage edges, typed as &lt;code&gt;FINETUNE_OF&lt;/code&gt;, &lt;code&gt;ADAPTER_OF&lt;/code&gt;, &lt;code&gt;MERGE_OF&lt;/code&gt; or &lt;code&gt;QUANTIZED_FROM&lt;/code&gt; from the &lt;code&gt;base_model&lt;/code&gt; tags;&lt;/li&gt;
&lt;li&gt;386,483 &lt;code&gt;TRAINED_ON&lt;/code&gt; edges to 66,243 datasets;&lt;/li&gt;
&lt;li&gt;47 &lt;code&gt;License&lt;/code&gt; nodes, each linked to the clauses it imposes, with the quote and the source URL for every clause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A license on Hugging Face is often just &lt;code&gt;other&lt;/code&gt;. 123,135 models use that form and put the real name in &lt;code&gt;license_name&lt;/code&gt;. 43,164 of them are under FLUX.1 [dev], which is non-commercial. If you only read the tag, you miss all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;242,170 models sit below a model under restricted terms: Llama, Gemma, Qwen, FLUX, Mistral, OpenRAIL, DeepSeek, CC BY-NC, and licenses that say non-commercial or research-only in their name.&lt;/p&gt;

&lt;p&gt;48,975 of those declare a permissive license, apache-2.0 or MIT or similar. They had 5.88 million downloads in the 30 days before the snapshot. More than half of them (25,627) have at least 10 downloads, so these are models people use. Only 346 ship any license or notice file.&lt;/p&gt;

&lt;p&gt;11,442 of the 48,975 say commercial use is fine while an ancestor's license says it isn't.&lt;/p&gt;

&lt;p&gt;The count is one Cypher query once every model is marked with what it inherits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m.inherits&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;m.license&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$permissive&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m.downloads&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marking is the interesting part. For each restricted license, a JavaScript UDF inside FalkorDB starts from every model under that license and walks down to every descendant. All 73,794 descendants of Qwen Research models take 0.4 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the wrong labels come from
&lt;/h2&gt;

&lt;p&gt;54% of the 48,975 carry one exact set of tags: &lt;code&gt;unsloth&lt;/code&gt;, &lt;code&gt;text-generation-inference&lt;/code&gt;, &lt;code&gt;license:apache-2.0&lt;/code&gt;. That's the model card template in Unsloth's &lt;code&gt;save.py&lt;/code&gt;, which writes &lt;code&gt;license: apache-2.0&lt;/code&gt; on every upload whatever the base model's license is. People fine-tune Llama or Qwen with Unsloth, press push, and the card says apache.&lt;/p&gt;

&lt;p&gt;Nobody did anything wrong on purpose. It's a default in a template, multiplied across tens of thousands of uploads. I wrote a patch that reads the license from the base model (following Unsloth's own mirrors up to the original) and falls back to &lt;code&gt;other&lt;/code&gt; when it can't. It covers both the library and Unsloth Studio, and Unsloth's own card and export tests pass with it.&lt;/p&gt;

&lt;p&gt;The graph also finds where a wrong label starts. 38,489 models declare a permissive license directly on top of a restricted parent. Below 2,438 of them sit 9,386 more models that copied the wrong label from their parent rather than writing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a graph and not the API
&lt;/h2&gt;

&lt;p&gt;The Hugging Face API lists a model's direct children, 1,000 per page. Finding all 21,639 descendants of Llama-3.1-8B that way takes 21,653 calls, about 3.7 hours at the 0.61 seconds per call I measured. FalkorDB returns the same set in 70 ms.&lt;/p&gt;

&lt;p&gt;A Python BFS over an edge list in memory would also be fast. What the graph gave me was one store holding lineage, datasets, licenses and clauses together, which serves a single model check and a Hub-wide audit from the same data and takes new edges without a rebuild. The obligations a model inherits come out of one query: walk the ancestors, follow each one's &lt;code&gt;LICENSED_UNDER&lt;/code&gt; edge, collect the clauses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;m.id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;lineage.ancestors&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;anc&lt;/span&gt;
&lt;span class="k"&gt;UNWIND&lt;/span&gt; &lt;span class="n"&gt;anc&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;x:&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:LICENSED_UNDER&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;l:&lt;/span&gt;&lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:IMPOSES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Clause&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;x.id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a.id&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l.permissive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;l.family&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a.depth&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;c.obligation&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;algo.SPpaths&lt;/code&gt; then gives the three shortest routes by which a license reaches a model, which is the proof shown on screen. PageRank over lineage says the most depended-on model on the whole Hub is &lt;code&gt;black-forest-labs/FLUX.1-dev&lt;/code&gt;, which is non-commercial.&lt;/p&gt;

&lt;p&gt;One more question I could only ask with a graph: what if Black Forest Labs relicensed FLUX.1-dev as Apache-2.0 tomorrow? FinePrint copies the 43,700-model family with &lt;code&gt;GRAPH.COPY&lt;/code&gt;, changes the license on the copy, walks it again and deletes the copy, in about half a second per request. 27,592 models would still be labelled non-commercial, because their own cards copied the FLUX label and each uploader would have to edit theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool: FinePrint
&lt;/h2&gt;

&lt;p&gt;All of this became FinePrint. Give it a model id and a use (commercial or not) and it answers ALLOW, CONDITIONS or BLOCK, with every inherited clause, the license text it comes from, and the path through the family tree.&lt;/p&gt;

&lt;p&gt;It fails closed. If any part of the chain is unknown, if an ancestor's license isn't one it recognises, or if the card makes a claim it couldn't resolve, it won't say ALLOW.&lt;/p&gt;

&lt;p&gt;Some models don't declare their base in tags at all. &lt;code&gt;deepseek-ai/DeepSeek-R1-Distill-Llama-8B&lt;/code&gt; says MIT and names no base model, so the tags alone give UNKNOWN. An LLM agent reads the card, quotes "DeepSeek-R1-Distill-Llama-8B is derived from Llama3.1-8B-Base", finds &lt;code&gt;meta-llama/Llama-3.1-8B&lt;/code&gt; in the graph, and writes a typed edge marked with its source and quote. The rules then see Llama 3.1 terms. The LLM never writes a query and never decides a verdict. It only adds evidence the rules can check, and every claim must quote the card word for word.&lt;/p&gt;

&lt;p&gt;The same decision runs in places where developers already are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an MCP server, so a coding agent can check a model, find a clean substitute of the same task and size, and draft the notice file;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fineprint.guard()&lt;/code&gt;, which stops a &lt;code&gt;from_pretrained&lt;/code&gt; download of a blocked model before the weights arrive;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fineprint check&lt;/code&gt;, a CLI that fails a CI build when a repo pulls in a model that is blocked for its use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does it work?
&lt;/h2&gt;

&lt;p&gt;I built a sealed test set of 120 models across seven groups (restricted terms two or more hops up, one hop up, clean, FLUX derivatives, merges, a random sample and so on). The gold labels came from a separate script that walks lineage through the live Hugging Face API, not through my dump or graph, and the file was hashed before any system ran.&lt;/p&gt;

&lt;p&gt;The number I care about is how often a system calls a restricted model clean, because that is the mistake that ships. Reading the declared license alone did it 33 times out of 69. Looking at direct parents, which is what the model page shows, did it 19 times. An LLM reading the model card did it 8 times.&lt;/p&gt;

&lt;p&gt;The strongest baseline was an LLM agent with the live Hub API as tools, allowed 10 calls per model, told to follow every base model up and check every license. It was good. It got more models right than FinePrint on one version of the labels (92 against 80), mostly because FinePrint answers UNKNOWN for models that declare no license at all. But it still called a restricted model clean 5 times, and all five were quantized copies of merge trees where the restricted ancestor sat many hops up. It ran out of calls, or hit a gated repo, before it got there. It needed a median 11 seconds and about 4 Hub calls per model.&lt;/p&gt;

&lt;p&gt;FinePrint called a restricted model clean 0 times, found the restricted terms in all 19 cases where they sit two or more hops up, and answered in a median 9 ms with no model calls. It says UNKNOWN when it doesn't know, which costs some answers and is the right trade for this kind of tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;These are differences between a declared license and upstream terms, not legal findings. Some of these models may have permission I can't see. Whether a non-commercial dataset's terms reach the weights trained on it is legally unsettled, and FinePrint marks those as contested and does not treat them as settled. The point is to show the question before you ship, with the evidence attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thanks
&lt;/h2&gt;

&lt;p&gt;FalkorDB for the database and the hackathon, and &lt;code&gt;cfahlgren1/hub-stats&lt;/code&gt; for the daily Hub snapshot that made a Hub-wide count possible. Along the way I hit a FalkorDB UDF bug: a &lt;code&gt;types&lt;/code&gt; list in &lt;code&gt;graph.traverse&lt;/code&gt; follows only its last type. The repro is in the repo.&lt;/p&gt;

&lt;p&gt;Every number in this post comes from a script listed in the repo's CLAIMS.md, and the whole thing rebuilds from one &lt;code&gt;reproduce.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Try it: &lt;a href="https://fineprint-plum.vercel.app" rel="noopener noreferrer"&gt;https://fineprint-plum.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/kinnuworks/fineprint" rel="noopener noreferrer"&gt;https://github.com/kinnuworks/fineprint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo video: &lt;a href="https://youtu.be/GyyAkWWM_AA" rel="noopener noreferrer"&gt;https://youtu.be/GyyAkWWM_AA&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for Graph Hacks: Context for AI Agents (WeMakeDevs x FalkorDB). I used an AI coding assistant while building it; the README says how.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>machinelearning</category>
      <category>database</category>
    </item>
    <item>
      <title>Seven in ten could read the label. One in three could take the right dose</title>
      <dc:creator>Dhiraj Kavuri</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:58:23 +0000</pubDate>
      <link>https://dev.to/kinnuworks/seven-in-ten-could-read-the-label-one-in-three-could-take-the-right-dose-2jb2</link>
      <guid>https://dev.to/kinnuworks/seven-in-ten-could-read-the-label-one-in-three-could-take-the-right-dose-2jb2</guid>
      <description>&lt;p&gt;In 2006, researchers handed 395 adults a pill bottle. The label said "Take two tablets by mouth twice daily." Then they asked each person to count out the pills they would take in a day.&lt;/p&gt;

&lt;p&gt;Among patients with low literacy, 71% could say the instruction back. Only 35% could count out the right number of pills (Davis et al., Annals of Internal Medicine).&lt;/p&gt;

&lt;p&gt;I keep coming back to that gap. People could read the words and still take the wrong dose.&lt;/p&gt;

&lt;p&gt;A page of labels like that one&lt;br&gt;
A hospital discharge sheet is a page or two of instructions like that label, handed over when a patient is tired, unwell and about to leave. There are about 35.7 million hospital admissions a year in the United States (American Hospital Association). The CDC counts more than 1.5 million emergency visits a year for harm from a medicine (CDC).&lt;/p&gt;

&lt;p&gt;Some people are hit harder than others:&lt;/p&gt;

&lt;p&gt;61.2 million people in the United States are 65 or older (Census Bureau). They land in an emergency department for a medicine problem more than twice as often as younger people.&lt;br&gt;
28.9 million speak English less than very well (Census Bureau).&lt;br&gt;
28% of adults aged 16 to 65 score at the lowest literacy level (NCES).&lt;br&gt;
Pictures work&lt;br&gt;
This part surprised me most. The evidence for pictures is old and strong.&lt;/p&gt;

&lt;p&gt;In one study, people remembered 14% of spoken medical instructions. With pictographs next to the same instructions, they remembered 85% (Houts et al.). In a randomized trial with 245 caregivers, picture instruction sheets cut the share of parents who measured a child's medicine wrong from 47.8% to 5.4% (Yin et al.).&lt;/p&gt;

&lt;p&gt;So why doesn't every patient get pictures? Because someone has to draw them, for every medicine, dose and language.&lt;/p&gt;

&lt;p&gt;Image models can draw them. They also miscount.&lt;br&gt;
An image model will draw a pictogram for any instruction you give it. I tried it for a hackathon. I asked for one spoonful of medicine and got eight spoons. Across 168 pictures of whole doses, the model drew the wrong number of pills 83 times.&lt;/p&gt;

&lt;p&gt;On a medicine card, a picture with three pills for a dose of two is worse than no picture.&lt;/p&gt;

&lt;p&gt;Concord: every picture has to pass a reading test&lt;br&gt;
So I built Concord around one rule. A generated picture is shown to a patient only after two other models, from two different companies, have looked at it with no hint and said in their own words what it shows. If they misread it, it is thrown away and drawn again.&lt;/p&gt;

&lt;p&gt;The image model is never asked to count. It draws one pill. The check confirms it is exactly one. Code repeats it for the dose, so the number on a card is exact.&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%2Fmmvrxp7gq9ycgkz5aoub.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%2Fmmvrxp7gq9ycgkz5aoub.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The check earns its place. Asked for a swollen foot, the model drew something both readers called "a red sock". That picture never reached a card.&lt;/p&gt;

&lt;p&gt;The same card in 70 languages&lt;br&gt;
A picture needs no translation. The words around it do, so they go through the same kind of check. One model writes the label or the medicine's name in the patient's language. A second model, which never saw the English, reads it back.&lt;/p&gt;

&lt;p&gt;I ran that on every prescribable medicine with a WHO classification code: 1,830 medicines in 69 languages, 126,270 names. 98% came back as the same medicine. The ones that did not stay in Latin letters on the card.&lt;/p&gt;

&lt;p&gt;So a patient who reads Hindi sees "Metformin 500 mg" and, under it, "मेटफॉर्मिन 500 mg".&lt;/p&gt;

&lt;p&gt;Try it&lt;br&gt;
Concord is free, needs no account, and the code is open.&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://concord-health.vercel.app" rel="noopener noreferrer"&gt;https://concord-health.vercel.app&lt;/a&gt;&lt;br&gt;
Code: &lt;a href="https://github.com/kinnuworks/concord" rel="noopener noreferrer"&gt;https://github.com/kinnuworks/concord&lt;/a&gt;&lt;br&gt;
It is a reading aid. It redraws what the sheet says and adds no medical advice. The next step is a comprehension study with patients and a pharmacist, and if you work in a pharmacy or on a ward I would like to hear from you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>healthtech</category>
      <category>opensource</category>
      <category>a11y</category>
    </item>
  </channel>
</rss>
