<?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: Oaida Adrian</title>
    <description>The latest articles on DEV Community by Oaida Adrian (@darksider4all_afa2428f63d0).</description>
    <link>https://dev.to/darksider4all_afa2428f63d0</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%2F4014906%2Fc97aa091-845d-4fe5-b6fd-5a98bf7a23fa.jpg</url>
      <title>DEV Community: Oaida Adrian</title>
      <link>https://dev.to/darksider4all_afa2428f63d0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darksider4all_afa2428f63d0"/>
    <language>en</language>
    <item>
      <title>VAT validation: three answers, not two</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Fri, 06 Nov 2026 22:51:59 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/vat-validation-three-answers-not-two-2eg0</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/vat-validation-three-answers-not-two-2eg0</guid>
      <description>&lt;p&gt;A VAT validator that answers "invalid" when the registry is down is worse than one that refuses to answer. It takes a good customer, marks them a compliance risk, and nobody notices until an invoice is queried.&lt;/p&gt;

&lt;p&gt;I built one against VIES — the European Commission's own VAT registry — and the interesting part was not the HTTP call. It was the three-way status, and the fact that VIES makes it easy to get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint is free and keyless
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://ec.europa.eu/taxation_customs/vies/rest-api/ms/{country}/vat/{number}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;RO14399840&lt;/code&gt; it answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isValid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VALID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DANTE INTERNATIONAL SA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MUNICIPIUL BUCUREŞTI, SECTOR 2 …"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;userError: "VALID"&lt;/code&gt;. The verdict is duplicated across two fields, and they disagree in meaning: &lt;code&gt;isValid&lt;/code&gt; is the answer, &lt;code&gt;userError&lt;/code&gt; carries a code. My first classifier read &lt;code&gt;userError&lt;/code&gt; first, found a non-empty string, and reported &lt;strong&gt;every valid number as invalid&lt;/strong&gt;. The fix is one line — trust &lt;code&gt;isValid&lt;/code&gt; when it is true, and only look at &lt;code&gt;userError&lt;/code&gt; when it is false:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isValid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userError&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;UNAVAILABLE&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONCURRENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The status that matters is the third one
&lt;/h2&gt;

&lt;p&gt;On the day I wrote this, two member states were not answering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DE811569869   -&amp;gt; unknown   MS_UNAVAILABLE
FR40303265045 -&amp;gt; unknown   MS_MAX_CONCURRENT_REQ   (then valid, twenty minutes later)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MS_UNAVAILABLE&lt;/code&gt;, &lt;code&gt;MS_MAX_CONCURRENT_REQ&lt;/code&gt;, &lt;code&gt;GLOBAL_MAX_CONCURRENT_REQ&lt;/code&gt;, &lt;code&gt;TIMEOUT&lt;/code&gt; are all "I could not answer". A two-state validator folds them into &lt;code&gt;invalid&lt;/code&gt;, and there is no way for the caller to tell the difference between "this company is not VAT registered" and "Germany is having a bad afternoon".&lt;/p&gt;

&lt;p&gt;So the output carries &lt;code&gt;status: valid | invalid | unknown&lt;/code&gt; and the raw &lt;code&gt;reason&lt;/code&gt; for the third. &lt;code&gt;unknown&lt;/code&gt; rows are never billed and never counted as a change — an outage should not look like movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring beats dumping
&lt;/h2&gt;

&lt;p&gt;The second half is a diff. Take a list of VAT numbers, validate them, and on the next run emit only the ones whose answer changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vatNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RO14399840"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DANTE INTERNATIONAL SA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isChange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That needs state outside the run. On Apify the default key-value store is per run, so a scheduled run always looks like a first run and never diffs anything — the state has to live in a &lt;strong&gt;named&lt;/strong&gt; store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_key_value_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-vat-validator-state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# {ts, payload, row}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quiet day then costs a handful of calls and produces an empty dataset, which is itself the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parse trap nobody expects
&lt;/h2&gt;

&lt;p&gt;VAT numbers are two letters plus a number — so the string &lt;code&gt;Dedeman&lt;/code&gt; parses cleanly as country &lt;code&gt;DE&lt;/code&gt; and number &lt;code&gt;DEMAN&lt;/code&gt;. A company name silently becomes a query. Real VAT numbers always carry digits (&lt;code&gt;ATU12345678&lt;/code&gt;, &lt;code&gt;ESA1234567&lt;/code&gt;, &lt;code&gt;NL123456789B01&lt;/code&gt;), so the guard is that at least three digits must survive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one is a test now, along with the &lt;code&gt;userError: "VALID"&lt;/code&gt; case, so neither can come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Concurrency 3, small delay: VIES throttles per member state, and a burst earns &lt;code&gt;MS_MAX_CONCURRENT_REQ&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;24 h cache per number by default; &lt;code&gt;cacheHours: 0&lt;/code&gt; forces a live check.&lt;/li&gt;
&lt;li&gt;13 unit tests, pure logic, no network, run inside the Docker build so a wrong verdict cannot ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The validator is published as an Apify actor (&lt;code&gt;darknezz/eu-vat-validator&lt;/code&gt;). If you are writing your own, the single most useful thing I can tell you is the one above: VIES answers "valid" with &lt;code&gt;userError: "VALID"&lt;/code&gt;, and it answers "Germany is down" with &lt;code&gt;isValid: false&lt;/code&gt;. Those two must not end up in the same bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/metering-an-mcp-server-by-answers-not-requests-what-leadgen-20-changed-12g"&gt;Metering an MCP server by answers, not requests: what Leadgen 2.0 changed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>tutorial</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Metering an MCP server by answers, not requests: what Leadgen 2.0 changed</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 28 Oct 2026 18:40:22 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/metering-an-mcp-server-by-answers-not-requests-what-leadgen-20-changed-12g</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/metering-an-mcp-server-by-answers-not-requests-what-leadgen-20-changed-12g</guid>
      <description>&lt;p&gt;Every MCP server that gets listed in a directory learns the same lesson within a week: the most frequent caller is not a customer. It is a crawler doing &lt;code&gt;initialize&lt;/code&gt; followed by &lt;code&gt;tools/list&lt;/code&gt;, deciding whether you deserve to be indexed, and moving on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://leadgen.adrianhomelab.com" rel="noopener noreferrer"&gt;Leadgen 2.0&lt;/a&gt; went live this week. It is not a price change and not a promo — it is the list of things that got in our own way when we used the platform, fixed.&lt;/p&gt;

&lt;p&gt;The most interesting one is the metering, because it is a category of bug every MCP server will hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probing should not spend a lookup
&lt;/h2&gt;

&lt;p&gt;Our allowances went up in this release: a free key now gives 50 lookups a day (it was 25), and without a key the MCP server answers 30 calls a day per IP (it was 5), with the REST door counted separately at 5. The two doors never spend each other's allowance, and every limit message quotes the door it came from.&lt;/p&gt;

&lt;p&gt;The part that needed fixing was &lt;strong&gt;what counts as a call&lt;/strong&gt;: the counter was incremented on every request to &lt;code&gt;/mcp&lt;/code&gt;, including the ones that never reach a tool.&lt;/p&gt;

&lt;p&gt;Two days of production logs, same window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2,333 requests to /mcp          ~100 distinct client IPs
  612  Uptime-Kuma              /health
  390  mcpbeat/0.1              directory crawler
  339  SentinelOracle/0.1       rank probe
  246  curl/7.88.1
  112  mcptry.com indexer
   64  AgentRankProbe/0.1

and, in the usage table: 4 quota rows at 0 calls, 1 real keyed lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A crawler doing &lt;code&gt;initialize&lt;/code&gt; + &lt;code&gt;tools/list&lt;/code&gt; and nothing else was opening the counter. A day full of robots could leave a real visitor without their free lookups — on a shared egress IP, they would even collide with each other.&lt;/p&gt;

&lt;p&gt;The rule we landed on: only answers that carry data (or a genuine not-found) count. The fix is a rule, not a heuristic: &lt;strong&gt;only answers that carry data (or a genuine not-found) count.&lt;/strong&gt; Everything else — &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;ping&lt;/code&gt;, notifications, input errors, refusals — is handed back. One owner for the counter (&lt;code&gt;record_call()&lt;/code&gt;), one function for the give-back (&lt;code&gt;refund_call()&lt;/code&gt;), and the enforcement point decides by the shape of the answer.&lt;/p&gt;

&lt;p&gt;We deliberately did not key it on the User-Agent. A rule like "don't charge mcpbeat" is a rule you rewrite every month, and it teaches nothing: the next crawler is one header away from looking like a browser. Meter the protocol, not the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passwordless sign-in that survives mail scanners
&lt;/h2&gt;

&lt;p&gt;Email plus a 6-digit code, in the app and on the site. No password to reset, no "confirm your email" step that fights with link prefetching.&lt;/p&gt;

&lt;p&gt;The trick is that the mailed link only &lt;strong&gt;opens a page&lt;/strong&gt; — it never confirms anything by being fetched. The address is confirmed by that page's POST, so a scanner that walks every link in an inbox confirms nothing. Same pattern we use for watchlist alerts: &lt;code&gt;pending&lt;/code&gt; → &lt;code&gt;confirmed&lt;/code&gt; on a form submit, never on a GET.&lt;/p&gt;

&lt;p&gt;An expired or already-used sign-in link now says so in plain words and offers a fresh one to the same address. The API key waits behind the same door: masked, with a copy button, and replaceable &lt;strong&gt;without&lt;/strong&gt; losing plan, usage or watchlists. That last part is a database decision, not a UI one — the key is a row's identity, not the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  One search box
&lt;/h2&gt;

&lt;p&gt;Company name, registration number (CUI), or website, in the same input. The front end classifies what you typed and routes it; the API keeps its explicit parameters. Users do not read parameter names, and a search box that requires you to know which field you are filling in is a form wearing a search box costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phone numbers, written the way you dial them
&lt;/h2&gt;

&lt;p&gt;Formatted to dial (&lt;code&gt;0744 700 293&lt;/code&gt;), with an international column. Each number carries its provenance — declared to ANAF, found on the company's own site, or listed elsewhere — and a number found on a website is rated high only when it matches the one the company declared. In a registry with millions of rows, "we have a number" and "this number is still that company's" are different claims, and the UI should not blur them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Big lists: compute in the database, not in the page
&lt;/h2&gt;

&lt;p&gt;Revenue and headcount bands are computed in the database, not in the page, so lists of thousands of companies stop being a progress bar. The bands no longer overlap either — a company lands in exactly one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer door and user door
&lt;/h2&gt;

&lt;p&gt;On the site, "Start free" goes to the app and "Get the free key" goes to the developer page — which is what &lt;code&gt;/start&lt;/code&gt; now is. The MCP server carries &lt;strong&gt;18 tools&lt;/strong&gt;, and scripts can still self-serve a key with &lt;code&gt;POST /register&lt;/code&gt;. Machines and people have different first clicks; pretending otherwise serves neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in one call
&lt;/h2&gt;

&lt;p&gt;Streamable HTTP is a &lt;strong&gt;stateful&lt;/strong&gt; protocol: you &lt;code&gt;initialize&lt;/code&gt;, keep the session id, then call. Two requests, no key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. open a session — the reply header carries Mcp-Session-Id&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - https://leadgen-mcp.adrianhomelab.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. call a tool with that id (a real one: this CUI is my own company)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://leadgen-mcp.adrianhomelab.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Mcp-Session-Id: &amp;lt;id from step 1&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"lookup_business","arguments":{"query":"55685084"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"55685084"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"OAIDA A. IOAN-ADRIAN
 PERSOANĂ FIZICĂ AUTORIZATĂ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"55685084"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"registrationCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"F2026045821000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"legalForm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"PFA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"county"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bucureşti"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eighteen tools sit behind that door — &lt;code&gt;lookup_business&lt;/code&gt;, &lt;code&gt;lookup_companies&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;find_company_by_domain&lt;/code&gt;, &lt;code&gt;lookup_financials&lt;/code&gt;, &lt;code&gt;extract_contacts&lt;/code&gt;, &lt;code&gt;watch_company&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;poll_watchlist&lt;/code&gt;, &lt;code&gt;get_usage&lt;/code&gt; and the rest. Docs: &lt;code&gt;https://leadgen.adrianhomelab.com/en/docs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 2.0 does not fix yet
&lt;/h2&gt;

&lt;p&gt;Being honest about the tail, because these are the parts still on the board:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Authorization: Bearer&lt;/code&gt; and &lt;code&gt;X-API-Key&lt;/code&gt; both work, but the docs do not say so — a cold caller guesses.&lt;/li&gt;
&lt;li&gt;A request for &lt;code&gt;/docs&lt;/code&gt; with no &lt;code&gt;Accept-Language&lt;/code&gt; still defaults to Romanian. English lives at &lt;code&gt;/en/docs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The anonymous wall is per source IP, so callers behind one egress share a bucket. That is a design limit, not a bug we can paper over.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build an MCP server, the metering item is the one I would copy first. It is a small amount of code and it decides whether your free tier is a loss leader or a denial of service against your own users.&lt;/p&gt;




&lt;p&gt;Leadgen serves Romanian company data over the ONRC registry and ANAF filings — 4.2M companies, lookups by CUI, name or website, from Python, curl or an agent. Prices are charged in euro; on the Romanian pages the amount in lei is shown next to them at the National Bank of Romania's reference rate, with the rate's date. Server: &lt;code&gt;https://leadgen-mcp.adrianhomelab.com/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one over 4.2M firms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M companies as queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I stopped scraping business directories and built an MCP server on official registry data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Romania's company registry, now callable from Claude and Cursor (MCP + REST)</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Fri, 16 Oct 2026 21:20:42 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f</guid>
      <description>&lt;p&gt;Last month I wrote about &lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;building a free API over Romania's company registry&lt;/a&gt; — 4.2 million firms from ONRC, plus ANAF data, because there was no free way to look a company up by CUI without opening four government portals.&lt;/p&gt;

&lt;p&gt;The part people actually replied about was the MCP server. So this post is the how-to: wiring Romanian company data into Claude, Cursor, or any MCP-capable client in about three minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an agent wants this
&lt;/h2&gt;

&lt;p&gt;If you do anything with Romanian companies — B2B sales, invoicing, compliance, KYB, market research — the workflow is always the same shape. Someone hands you a name or a CUI. You go to the registry. You copy fields into a doc. You check whether the firm is still active. You try to find the phone number.&lt;/p&gt;

&lt;p&gt;That is exactly the loop an agent should close on its own. The only reason it can't is that the registry speaks Romanian HTML tables and CAPTCHA, not JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config
&lt;/h2&gt;

&lt;p&gt;Point your client at the streamable HTTP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"leadgen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://leadgen-mcp.adrianhomelab.com/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for Cursor and Claude Code. For a client that only reads command-line servers, wrap it with &lt;code&gt;mcp-remote&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; mcp-remote https://leadgen-mcp.adrianhomelab.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No key is required to start. Anonymous access is 5 queries a day per IP — enough to try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent gets
&lt;/h2&gt;

&lt;p&gt;Sixteen tools, all read-only. The ones I use most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_business&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One firm by CUI or name: status, address, CAEN codes, ANAF VAT state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_companies&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Batch up to 100 CUIs, one row out per row in, same order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;match_companies&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fuzzy match up to 500 rows (name + county + domain) against the registry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find_company_by_domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which company owns this website — with SPF/DMARC audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_director&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every firm a person is registered at — useful for due diligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_financials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turnover, profit, employees, year by year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;watch_company&lt;/code&gt; / &lt;code&gt;watch_caen&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Monitor a firm, or an entire CAEN activity code, for changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A real exchange looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; which company runs emag.ro, and is its VAT still active?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent:&lt;/strong&gt; &lt;em&gt;(calls &lt;code&gt;find_company_by_domain&lt;/code&gt;, then &lt;code&gt;lookup_business&lt;/code&gt;)&lt;/em&gt; — Dante International SA, CUI 14399840, Bucharest, VAT-active as of this month.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;watch_*&lt;/code&gt; pair is the sleeper feature: it turns the registry into a feed. Watch a competitor's CUI and you hear about a director change, a branch opening, or a new CAEN code without polling anything yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'd rather not use MCP
&lt;/h2&gt;

&lt;p&gt;Same data over plain REST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://leadgen.adrianhomelab.com/lookup_business?query=14399840"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: lg_your_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every response carries &lt;code&gt;RateLimit-Limit&lt;/code&gt;, &lt;code&gt;RateLimit-Remaining&lt;/code&gt; and &lt;code&gt;RateLimit-Reset&lt;/code&gt;, and &lt;code&gt;GET /usage&lt;/code&gt; tells you where you stand without spending a query. Interactive docs with try-it-out: &lt;a href="https://leadgen.adrianhomelab.com/docs" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com/docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No key: &lt;strong&gt;5 queries/day per IP&lt;/strong&gt;. Free key from &lt;a href="https://leadgen.adrianhomelab.com/start" rel="noopener noreferrer"&gt;/start&lt;/a&gt;: &lt;strong&gt;25/day&lt;/strong&gt;. Resets 00:00 UTC.&lt;/li&gt;
&lt;li&gt;Every endpoint is free during launch. When paid plans arrive, Pro (€9/month) takes the full 2008–2025 financial history, ratios, health scores, revenue/employee/VAT filters, CSV export and lists over 25 rows. The free tier keeps the company profile, status and risk, ANAF data, last-year figures, and lists up to 25 rows. Nothing free today becomes paid without warning.&lt;/li&gt;
&lt;li&gt;Batches count per row, and a batch larger than your remaining quota is refused whole rather than half-executed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I'm giving it away
&lt;/h2&gt;

&lt;p&gt;The registry data is public. What isn't public is a sane interface to it, and every Romanian developer has now paid the same tax of scraping HTML tables or buying a €200/month KYB subscription for data that came out of a government PDF.&lt;/p&gt;

&lt;p&gt;So the free tier is real, not a trial: it exists because the alternative for most people is not paying me, it's giving up on the lookup. The paid tier is for teams who need depth — a decade of financials, ratios, bulk export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Two minutes, no card:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a free key at &lt;a href="https://leadgen.adrianhomelab.com/start" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com/start&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Add the MCP config above, or call the REST endpoint with &lt;code&gt;X-API-Key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ask your agent about a Romanian company&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Landing page (EN): &lt;a href="https://leadgen.adrianhomelab.com/en" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com/en&lt;/a&gt; · MCP server card: &lt;a href="https://leadgen-mcp.adrianhomelab.com/" rel="noopener noreferrer"&gt;leadgen-mcp.adrianhomelab.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a solo project, so if something is missing — a field you need, an endpoint that doesn't exist, a county your data is wrong for — reply here or open an issue from the &lt;code&gt;/feedback&lt;/code&gt; page. I read both.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:33:43 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l</guid>
      <description>&lt;h1&gt;
  
  
  Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)
&lt;/h1&gt;

&lt;p&gt;I run a side hustle building data tools for AI agents. A few weeks ago I needed&lt;br&gt;
Romanian company data — the kind of thing every lead-gen tool wants: who's&lt;br&gt;
registered, their CAEN activity codes, legal representatives, status, address,&lt;br&gt;
website.&lt;/p&gt;

&lt;p&gt;Turns out that data has no free API. Romania's official business registry&lt;br&gt;
(&lt;strong&gt;ONRC&lt;/strong&gt;) publishes everything as open data — a monthly CSV dump on&lt;br&gt;
&lt;code&gt;data.gov.ro&lt;/code&gt; — but there's no programmatic way to query it. The only access is&lt;br&gt;
commercial scrapers, or scraping the registry website yourself.&lt;/p&gt;

&lt;p&gt;So I built the missing API. Here's the whole thing, from a 1.5 GB government&lt;br&gt;
CSV dump to a live MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data is already free — it's just not queryable
&lt;/h2&gt;

&lt;p&gt;Romania publishes its entire registry as open data. Every month, &lt;code&gt;data.gov.ro&lt;/code&gt;&lt;br&gt;
drops a snapshot: &lt;strong&gt;4,196,860 companies&lt;/strong&gt;, 19.3M CAEN activity records, 3.68M&lt;br&gt;
legal representatives, 4.64M status entries. For 2025, it also publishes full&lt;br&gt;
&lt;strong&gt;financial statements&lt;/strong&gt; — balance sheets, revenue, headcount, debt.&lt;/p&gt;

&lt;p&gt;The problem is ergonomics, not availability. The files are pipe-delimited CSVs,&lt;br&gt;
a gigabyte and a half each, with no index and no API. You can download them,&lt;br&gt;
but then you have a pile of text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning a CSV dump into a queryable registry
&lt;/h2&gt;

&lt;p&gt;The whole pipeline is three loaders:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;load_onrc.py&lt;/code&gt;&lt;/strong&gt; — streams the CKAN CSVs straight into SQLite (no pandas
loading 1.5 GB into RAM).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;load_nomenclatoare.py&lt;/code&gt;&lt;/strong&gt; — pulls the CAEN activity + status code
decode tables, so &lt;code&gt;0125&lt;/code&gt; renders as "cultivation of fruit trees" instead of
a number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;load_financiare.py&lt;/code&gt;&lt;/strong&gt; — loads the 2025 financial statements, keyed by CUI
(tax code).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the important bit: &lt;strong&gt;search&lt;/strong&gt;. Romanian names are full of diacritics —&lt;br&gt;
&lt;code&gt;PAVĂL&lt;/code&gt;, &lt;code&gt;ȘTEFAN&lt;/code&gt;, &lt;code&gt;ţ&lt;/code&gt;. If you search &lt;code&gt;paval&lt;/code&gt;, a naive &lt;code&gt;LIKE&lt;/code&gt; misses &lt;code&gt;PAVĂL&lt;/code&gt;.&lt;br&gt;
So I built an &lt;strong&gt;FTS5 full-text index&lt;/strong&gt; with diacritic-insensitive folding:&lt;br&gt;
&lt;code&gt;ă â î ș ț&lt;/code&gt; fold to &lt;code&gt;a a i s t&lt;/code&gt; on &lt;em&gt;both&lt;/em&gt; the index and the query. Search&lt;br&gt;
&lt;code&gt;popescu&lt;/code&gt; and you match &lt;code&gt;Popéscu&lt;/code&gt;; search &lt;code&gt;paval&lt;/code&gt; and you match &lt;code&gt;PAVĂL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is a 3.5 GB SQLite database that answers "who is this company?" in&lt;br&gt;
milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two surfaces, same data
&lt;/h2&gt;

&lt;p&gt;I exposed it two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A REST API&lt;/strong&gt; (&lt;a href="https://leadgen.adrianhomelab.com/" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com&lt;/a&gt;) — plain JSON over HTTP, with
Swagger docs. &lt;code&gt;GET /lookup_business?query=DEDEMAN&lt;/code&gt;, done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP server&lt;/strong&gt; (&lt;a href="https://leadgen-mcp.adrianhomelab.com/" rel="noopener noreferrer"&gt;&lt;code&gt;leadgen-mcp.adrianhomelab.com&lt;/code&gt;&lt;/a&gt; — endpoint &lt;code&gt;/mcp&lt;/code&gt;) — Streamable HTTP, so any
MCP-capable agent (Claude Desktop, Cursor, etc.) can query it natively with
16 tools (as of October 2026), among them &lt;code&gt;lookup_business&lt;/code&gt;, &lt;code&gt;lookup_companies&lt;/code&gt;,
&lt;code&gt;match_companies&lt;/code&gt;, &lt;code&gt;lookup_by_category&lt;/code&gt;, &lt;code&gt;lookup_director&lt;/code&gt;, &lt;code&gt;lookup_financials&lt;/code&gt;,
&lt;code&gt;extract_contacts&lt;/code&gt; and &lt;code&gt;lookup_domain&lt;/code&gt;. The full list is in the
&lt;a href="https://leadgen.adrianhomelab.com/docs?src=devto" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are free: 5 lookups a day with no key at all, or 25 a day with a free API&lt;br&gt;
key from &lt;a href="https://leadgen.adrianhomelab.com/start" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com/start&lt;/a&gt;&lt;br&gt;
— one email address, no payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The freemium shape
&lt;/h2&gt;

&lt;p&gt;Registry identity (name, CUI, address, CAEN, directors, status) is free. The&lt;br&gt;
&lt;strong&gt;financial statements&lt;/strong&gt; — revenue, profit, employees, assets, debt — are the&lt;br&gt;
paid tier. The scaffolding is already in the repo (&lt;code&gt;monetization.py&lt;/code&gt;), held&lt;br&gt;
behind a single &lt;code&gt;MONETIZATION_ENABLED&lt;/code&gt; flag. Flip it and paid tools require a&lt;br&gt;
bearer key; free tools get rate-limited.&lt;/p&gt;

&lt;p&gt;The point of the flag: prove agents actually &lt;em&gt;want&lt;/em&gt; this before I wire billing.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open data ≠ accessible data.&lt;/strong&gt; The value isn't the data — it's the
diacritic-insensitive search, the join across five tables, and making it
one HTTP call away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP is a distribution channel, not just a protocol.&lt;/strong&gt; Wrapping the same
SQLite DB as an MCP server instantly made it usable by any agent. That's the
wedge: the registry is one country, but the pattern generalises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the flag before the billing.&lt;/strong&gt; Monetization scaffolding costs nothing
to build and keeps the door open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it at &lt;a href="https://leadgen.adrianhomelab.com/" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com&lt;/a&gt; — a free key takes one email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data is Romania's — the queryability is the product.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Companion posts in the same build series:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt; — the REST API deep-dive and the diacritics/FTS5 traps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt; — why official open data beats directory scraping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it on Apify without hosting anything: &lt;a href="https://apify.com/darknezz/ro-business-data-mcp" rel="noopener noreferrer"&gt;Ro Business Data MCP&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>romania</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 26 Aug 2026 11:39:48 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3</guid>
      <description>&lt;p&gt;Every Romanian company is registered in the &lt;strong&gt;ONRC&lt;/strong&gt; (the national trade register), and the state publishes the whole thing as open data on &lt;a href="https://data.gov.ro" rel="noopener noreferrer"&gt;data.gov.ro&lt;/a&gt;. That's 4.2 million companies, 19.3 million CAEN activity records, 3.68 million legal representatives and 4.64 million status entries — a genuinely useful corpus for lead generation, market analysis and "who actually owns this company" lookups.&lt;/p&gt;

&lt;p&gt;The catch: it ships as a pile of enormous CSVs, and the moment you try to &lt;em&gt;query&lt;/em&gt; it, Romanian orthography punches you in the face. I built a small free API around it, and this is how.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source
&lt;/h2&gt;

&lt;p&gt;No scraping, no vendor. The official snapshot is a set of CKAN files, the biggest of which is &lt;code&gt;OD_FIRME.csv&lt;/code&gt; at ~690 MB and &lt;code&gt;OD_CAEN_AUTORIZAT.csv&lt;/code&gt; at ~425 MB. Combined, raw, that's well over a gigabyte of flat files. Loaded into SQLite with a full-text index it becomes a 3.6 GB database that answers a search in milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;The load is boring and that's the point: stream the CSVs into SQLite, then create an &lt;strong&gt;FTS5&lt;/strong&gt; virtual table so &lt;code&gt;lookup_business("dedeman")&lt;/code&gt; is a real full-text query rather than a &lt;code&gt;LIKE '%dedeman%'&lt;/code&gt; scan. Two tables matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;firme&lt;/code&gt; — companies, keyed by &lt;strong&gt;CUI&lt;/strong&gt; (the tax identifier) and &lt;strong&gt;registration code&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reprezentanti_legali&lt;/code&gt; — the legal representatives, so you can search &lt;em&gt;by person&lt;/em&gt; to find every company a director is attached to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that sits a thin &lt;strong&gt;FastAPI&lt;/strong&gt; wrapper exposing two read-only endpoints — no auth, no keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# by company name or CUI&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://leadgen.adrianhomelab.com/lookup_business?query=DEDEMAN&amp;amp;max_results=3"&lt;/span&gt;

&lt;span class="c"&gt;# by director / legal-representative name&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://leadgen.adrianhomelab.com/lookup_director?name=popescu%20ion"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The traps (this is the real content)
&lt;/h2&gt;

&lt;p&gt;Three things will bite you, in order of pain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Diacritics.&lt;/strong&gt; Romanian uses &lt;code&gt;ă â î ș ț&lt;/code&gt;, and the registry stores them faithfully. &lt;code&gt;Popescu&lt;/code&gt; and &lt;code&gt;Popéscu&lt;/code&gt; are different strings to a naive matcher — but a human searching for "Popescu" expects both. The FTS index has to be &lt;strong&gt;diacritic-insensitive&lt;/strong&gt;: normalize &lt;code&gt;ș&lt;/code&gt;→&lt;code&gt;s&lt;/code&gt;, &lt;code&gt;ț&lt;/code&gt;→&lt;code&gt;t&lt;/code&gt;, &lt;code&gt;ă&lt;/code&gt;→&lt;code&gt;a&lt;/code&gt; on both the indexed text &lt;em&gt;and&lt;/em&gt; the query. Miss this and your "obvious" search returns nothing for half the surnames in the country.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. CUI vs registration code.&lt;/strong&gt; Every firm has a CUI (digits, sometimes zero-padded) &lt;em&gt;and&lt;/em&gt; a registration code like &lt;code&gt;J1992002621040&lt;/code&gt; (court + year + serial). They are not interchangeable, and newcomers constantly pass one where the API expects the other. Supporting both on a single &lt;code&gt;query&lt;/code&gt; parameter means testing numeric input against the CUI column first, then falling back to the registration code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. CSV scale and encoding.&lt;/strong&gt; The raw dumps are &lt;code&gt;;&lt;/code&gt;-delimited and large enough that &lt;code&gt;csv.DictReader&lt;/code&gt; in a tight loop will keep you up all night. Stream with &lt;code&gt;csv.reader&lt;/code&gt;, batch your &lt;code&gt;executemany&lt;/code&gt; inserts, and watch your transaction size — a single 690 MB file has to be committed incrementally or SQLite's WAL balloons.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out of it
&lt;/h2&gt;

&lt;p&gt;A live, free endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /lookup_business?query=&amp;lt;name|CUI&amp;gt;&amp;amp;max_results=N&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /lookup_director?name=&amp;lt;name&amp;gt;&amp;amp;max_results=N&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /docs&lt;/code&gt; — interactive OpenAPI docs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /start&lt;/code&gt; — free API key: 25 lookups/day for an email address&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same data is also available as an &lt;strong&gt;MCP server&lt;/strong&gt; (for AI agents) at &lt;a href="https://leadgen-mcp.adrianhomelab.com/" rel="noopener noreferrer"&gt;leadgen-mcp.adrianhomelab.com&lt;/a&gt; — Streamable HTTP at the &lt;code&gt;/mcp&lt;/code&gt; path. If you're doing anything with Romanian business data — or you just want a worked example of turning a big government CSV dump into a queryable service — the endpoint is live and free to hit. Try it at &lt;a href="https://leadgen.adrianhomelab.com/" rel="noopener noreferrer"&gt;leadgen.adrianhomelab.com&lt;/a&gt; — a free key takes one email.&lt;/p&gt;

&lt;p&gt;Run it on Apify without hosting anything: &lt;a href="https://apify.com/darknezz/ro-business-data-mcp" rel="noopener noreferrer"&gt;Ro Business Data MCP&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>sqlite</category>
      <category>api</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:11:51 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj</guid>
      <description>&lt;p&gt;US public companies file everything with the SEC, and EDGAR is free to use. But the raw archive is hostile to analysis: filings are HTML documents with table-heavy layouts, and the full-text search UI is built for humans, not pipelines. I built an actor that turns a ticker into a clean corpus of JSON records — one per filing — ready for financial alt-data, risk analysis, RAG corpora, or LLM fine-tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source
&lt;/h2&gt;

&lt;p&gt;No API key, no data vendor. Three official SEC endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;company_tickers.json&lt;/code&gt; — ticker to CIK resolution&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;EDGAR full-text search JSON API&lt;/strong&gt; (&lt;code&gt;efts.sec.gov/LATEST/search-index&lt;/code&gt;) — query by ticker, form type, and date window&lt;/li&gt;
&lt;li&gt;the SEC Archives for the documents themselves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two traps surfaced immediately. First, &lt;strong&gt;SEC fair-access enforcement is real&lt;/strong&gt;: a bare curl or python-requests User-Agent gets a 403, while a descriptive UA (&lt;code&gt;Darknezz Research admin@…&lt;/code&gt;) gets a 200. The actor sets a descriptive UA, paces at ~5 requests/second (well under the 10 rps limit), and backs off on 429s. Second, the &lt;code&gt;ciks&lt;/code&gt; search parameter &lt;strong&gt;requires the 10-digit zero-padded CIK&lt;/strong&gt; — &lt;code&gt;320193&lt;/code&gt; returns zero hits, &lt;code&gt;0000320193&lt;/code&gt; returns the filing. Padding is mandatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structuring
&lt;/h2&gt;

&lt;p&gt;Raw HTML is only the beginning. Each filing becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt; — accession number, ticker, company, CIK, form type, filing date, period ending, 8-K event items, document and index URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sections&lt;/strong&gt; — split on Item headings (&lt;code&gt;Item 1.&lt;/code&gt;, &lt;code&gt;Item 1A.&lt;/code&gt;, &lt;code&gt;Item 7.&lt;/code&gt;, &lt;code&gt;Item 2.02&lt;/code&gt;…), per-section capped so one enormous filing can't blow a record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk factors&lt;/strong&gt; — the full 10-K/10-Q Item 1A text, verified on a real Apple 10-K at the 60k-character cap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holdings&lt;/strong&gt; — 13F-HR InfoTable XML parsed into issuer, CUSIP, value, shares, and voting authority. The XML is namespace-prefixed (&lt;code&gt;&amp;lt;ns1:infoTable&amp;gt;&lt;/code&gt;), so every regex is namespace-tolerant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summary&lt;/strong&gt; — a deterministic, LLM-ready markdown digest: metadata, section map, risk excerpt, holdings preview. No external LLM call, no extra cost&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AAPL 8-K ×3&lt;/strong&gt; — sections Item 2.02 and 9.01, summaries 476 chars each&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AAPL 10-K ×1&lt;/strong&gt; — 10+ sections, Item 1A risk factors 60,038 chars, summary 3,267 chars&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPM 13F-HR&lt;/strong&gt; — 34,064 positions parsed locally; the mega-manager cap (20,000 positions) proven, with &lt;code&gt;holdingsCount&lt;/code&gt; and &lt;code&gt;holdingsTruncated&lt;/code&gt; flags so the record stays under Apify's item size limit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All cloud runs succeeded with non-zero, well-formed output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Each filing yields its &lt;strong&gt;primary document only&lt;/strong&gt;; exhibits (press releases, contracts) stay one click away via the filing index URL.&lt;/li&gt;
&lt;li&gt;The summary is deterministic, not generative — great for pipelines, not a substitute for an LLM pass.&lt;/li&gt;
&lt;li&gt;Mega-manager 13Fs are truncated at 20,000 positions (flagged), because a 9 MB JSON record doesn't ship.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/sec-edgar-filings-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;SEC EDGAR Filings Scraper on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>finance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Price &amp; Competitor Monitor That Diffs Shopify and AliExpress Prices</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:11:47 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l</guid>
      <description>&lt;p&gt;Most price watching is a spreadsheet: paste a URL, check it today, forget it tomorrow. The moment you want a &lt;em&gt;history&lt;/em&gt; — what did this product cost a month ago, did it dip last Tuesday — snapshots are useless. I built an actor that keeps a real per-URL price timeline, diffs every run against the last one, and pings a webhook the instant something changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;For each URL the actor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetches the product&lt;/strong&gt; — Shopify stores expose &lt;code&gt;/products/&amp;lt;handle&amp;gt;.json&lt;/code&gt; plus &lt;code&gt;/cart.js&lt;/code&gt; for the store currency; AliExpress item pages are client-rendered, so the price comes from the search page matched back by product id&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads the previous snapshot&lt;/strong&gt; from a durable key-value store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computes the diff&lt;/strong&gt; — &lt;code&gt;NEW&lt;/code&gt;, &lt;code&gt;PRICE_CHANGED&lt;/code&gt;, &lt;code&gt;AVAILABILITY_CHANGED&lt;/code&gt;, or &lt;code&gt;UNCHANGED&lt;/code&gt;, with absolute and percentage change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appends the new point&lt;/strong&gt; to history, pruning anything older than &lt;code&gt;historyDays&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emits one row per URL&lt;/strong&gt; with snapshot + diff + recent history, and POSTs a compact JSON alert to your webhook if the row changed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first run of a URL is the baseline (&lt;code&gt;NEW&lt;/code&gt;, no alert). Alerts fire only on real changes after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The persistence trap
&lt;/h2&gt;

&lt;p&gt;This is the part that bit: Apify's &lt;strong&gt;default key-value store is per-run for API-started runs&lt;/strong&gt; — it's purged at run start and deleted after the run. A naive implementation would lose its history between runs and re-emit everything as &lt;code&gt;NEW&lt;/code&gt; every time. The fix is a &lt;strong&gt;named store&lt;/strong&gt;: &lt;code&gt;Actor.open_key_value_store(name='price-competitor-monitor-history')&lt;/code&gt; (the &lt;code&gt;name=&lt;/code&gt; argument is keyword-only in SDK 3.4). That store persisted across three cloud runs in testing, which is what turns scheduled runs into a real timeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;Three URLs — two Shopify stores and an AliExpress dock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run 1: allbirds &lt;strong&gt;NEW 91.00 USD&lt;/strong&gt;, colourpop &lt;strong&gt;NEW 29.00 USD&lt;/strong&gt;, non-zero ✓ (the AliExpress item page was x5sec-challenged from the datacenter egress — honest error row, no false price)&lt;/li&gt;
&lt;li&gt;Run 2: both Shopify products &lt;strong&gt;UNCHANGED&lt;/strong&gt; — durable history proven in the cloud&lt;/li&gt;
&lt;li&gt;Run 3 (with a modified snapshot): colourpop &lt;strong&gt;PRICE_CHANGED, previous 36.25 → 29.00, −20%, direction down, alert true&lt;/strong&gt; ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fetch that fails cleanly (blocked page, deleted product) emits a &lt;code&gt;status: "error"&lt;/code&gt; row and leaves the previous snapshot untouched — a transient block never masquerades as a price crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AliExpress item pages are fully client-rendered: the price is recovered from the search page by product id, so an item absent from search is reported without a price until it reappears.&lt;/li&gt;
&lt;li&gt;Availability on AliExpress isn't reported yet (&lt;code&gt;available: null&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;History is capped at 500 points per URL as a safety limit.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/price-competitor-monitor" rel="noopener noreferrer"&gt;&lt;strong&gt;Price &amp;amp; Competitor Monitor on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>ecommerce</category>
      <category>automation</category>
    </item>
    <item>
      <title>Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:11:46 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32</guid>
      <description>&lt;p&gt;Raw web pages are terrible training data. Nav bars, cookie banners, "related articles" and ads drown the signal, and near-identical syndicated text pollutes the corpus. If you're fine-tuning a domain model — legal reasoning, medical QA, financial analysis — you want clean vertical text with provenance, not a pile of HTML. I built an actor that goes from seed URLs to a token-aware JSONL dataset in one run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;seed URLs → crawl (same-domain BFS) → clean (boilerplate strip)
  → dedupe (exact + near) → token-aware chunk → JSONL with provenance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three steps matter most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Boilerplate strip&lt;/strong&gt; — drop &lt;code&gt;script&lt;/code&gt;/&lt;code&gt;style&lt;/code&gt;/&lt;code&gt;nav&lt;/code&gt;/&lt;code&gt;footer&lt;/code&gt;/&lt;code&gt;header&lt;/code&gt;/&lt;code&gt;aside&lt;/code&gt; and utility blocks (breadcrumbs, shares, comments, menus). One detail that earned its keep: paragraphs where more than half the links are anchor text get dropped too — those are link farms, not content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedupe, exact and near&lt;/strong&gt; — exact duplicates collapse by normalised hash; near-duplicates by 6-gram Jaccard similarity (default 0.95), so syndicated copies of the same opinion appear once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token-aware chunking&lt;/strong&gt; — chunks split at paragraph boundaries and hard-split on sentence/word boundaries only when a single paragraph exceeds the budget (default 512 tokens). Each chunk stays coherent, which is what fine-tuning actually wants.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every record carries &lt;code&gt;source&lt;/code&gt; (hostname), &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;domain&lt;/code&gt; (your vertical), &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;chunk_index&lt;/code&gt; — so you can filter, cite, or re-weight the corpus later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seed-source reality check
&lt;/h2&gt;

&lt;p&gt;Not every legal text source works from a datacenter IP. CourtListener sits behind an AWS WAF JavaScript challenge, and Justia 403s datacenter egress — both unusable as plain-HTTP seeds. &lt;strong&gt;Cornell LII&lt;/strong&gt; (&lt;code&gt;law.cornell.edu/supremecourt/text/…&lt;/code&gt;) serves full opinions as clean HTML and works beautifully. The honest move was documenting that in the README and defaulting the examples to LII rather than pretending every source is reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;Three LII Supreme Court opinions (Dobbs, Bruen, WV v EPA):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local: 559 chunks, all 6 provenance keys present, chunk sizes 63–2,054 chars, zero duplicates after global dedupe&lt;/li&gt;
&lt;li&gt;Cloud (2 seeds): &lt;strong&gt;SUCCEEDED, 443 items&lt;/strong&gt;, every record with non-empty text, &lt;code&gt;domain=legal&lt;/code&gt;, source &lt;code&gt;law.cornell.edu&lt;/code&gt;, 0 duplicate texts&lt;/li&gt;
&lt;li&gt;KV store: &lt;code&gt;output.jsonl&lt;/code&gt; — 443 lines / ~811 KB, plus a &lt;code&gt;SUMMARY&lt;/code&gt; record: 443 chunks, 2 pages, ~173,000 estimated tokens, no failed sources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Seed URLs must serve server-rendered HTML. JS-only SPAs and WAF'd sites need a browser-rendering actor instead.&lt;/li&gt;
&lt;li&gt;Chunk size is an estimate (~4 chars/token), not a byte-exact tokeniser count.&lt;/li&gt;
&lt;li&gt;Near-dedupe is O(n²) in chunk count — cap large crawls with &lt;code&gt;maxChunksPerPage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The crawler follows same-domain links only; cross-domain citations aren't chased.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/vertical-corpus-builder" rel="noopener noreferrer"&gt;&lt;strong&gt;Vertical Corpora Builder on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>data</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Change &amp; Alert Engine That Webhooks Any Diff on Any URL</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:11:44 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip</guid>
      <description>&lt;p&gt;Most data jobs re-pull the whole source every run and waste time and money on data that didn't move. A change-detection engine wakes up, checks what actually changed, and pushes only the diff. I built a generic one: point it at any URL — a JSON feed, an RSS/Atom feed, or a plain HTML page — and it emits only the changes, with before and after values, to the dataset and/or your webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Poll&lt;/strong&gt; — fetch &lt;code&gt;sourceUrl&lt;/code&gt; once per run (the interval is your schedule; the actor is a single-shot poll)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt; — content is sniffed automatically: JSON arrays used directly; object feeds auto-detect common list keys (&lt;code&gt;items&lt;/code&gt;, &lt;code&gt;results&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;records&lt;/code&gt;…) or take an &lt;code&gt;itemsPath&lt;/code&gt; dot path; RSS/Atom parsed with stable ids (guid or link); HTML becomes one monitored item keyed on a content hash&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff&lt;/strong&gt; — each item gets a stable &lt;code&gt;item_id&lt;/code&gt; (from &lt;code&gt;idField&lt;/code&gt;, natural keys like &lt;code&gt;guid&lt;/code&gt;/&lt;code&gt;link&lt;/code&gt;/&lt;code&gt;url&lt;/code&gt;/&lt;code&gt;title&lt;/code&gt;, or a content-hash fallback) and a content hash. New id → &lt;code&gt;added&lt;/code&gt;; same id, different hash → &lt;code&gt;modified&lt;/code&gt; with full before/after; id missing → &lt;code&gt;removed&lt;/code&gt; (only with &lt;code&gt;includeRemovals: true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emit&lt;/strong&gt; — changed items go to the dataset, one per item, plus one batched webhook POST per run that has changes. &lt;strong&gt;Nothing is emitted on a clean poll.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The delivery guarantee
&lt;/h2&gt;

&lt;p&gt;At-least-once is the detail I care most about: state is persisted &lt;strong&gt;only after a successful webhook POST&lt;/strong&gt; (or immediately when no webhook is configured). If the POST fails, the run FAILS and the next run re-emits the same changes — an alert is never silently lost. Within a run, duplicate item ids collapse to the last occurrence, so a feed that repeats entries won't double-notify.&lt;/p&gt;

&lt;h2&gt;
  
  
  The persistence trap
&lt;/h2&gt;

&lt;p&gt;Same lesson as the sibling price monitor: Apify's &lt;strong&gt;default key-value store is per-run for API-started runs&lt;/strong&gt;, so state kept there vanishes between runs and the second run re-emits everything as &lt;code&gt;added&lt;/code&gt;. The fix is a named store — &lt;code&gt;Actor.open_key_value_store(name='change-alert-state')&lt;/code&gt; (keyword-only &lt;code&gt;name=&lt;/code&gt; in SDK 3.4, account-scoped, persists across runs).&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BBC News RSS&lt;/strong&gt;: run 1 → &lt;strong&gt;41 added&lt;/strong&gt; (real titles and links); run 2 → &lt;strong&gt;0 items, zero changes&lt;/strong&gt; ✓&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled changed source&lt;/strong&gt; (an Apify dataset with a pre-signed public URL — gist raw CDN served stale content to datacenter IPs, so a dataset is the deterministic route): run A → 2 added; append a modified g2 and a new g3 → run B → &lt;strong&gt;modified g2 (price 20 → 25) + added g3&lt;/strong&gt;; run C → 0 ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The engine compares current fetch vs last persisted fetch — items that leave the feed are only reported with &lt;code&gt;includeRemovals: true&lt;/code&gt; (off by default, because first-page rotation is often noise).&lt;/li&gt;
&lt;li&gt;JSON items without any natural id fall back to a content hash; any edit then looks like an add+remove pair — set &lt;code&gt;idField&lt;/code&gt; for clean &lt;code&gt;modified&lt;/code&gt; detection.&lt;/li&gt;
&lt;li&gt;Feeds with per-item volatile timestamps can look "modified" every poll — list those fields in &lt;code&gt;ignoreFields&lt;/code&gt; to hash only what matters.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/change-alert-engine" rel="noopener noreferrer"&gt;&lt;strong&gt;Change &amp;amp; Alert Engine on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:11:43 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb</guid>
      <description>&lt;p&gt;Romania's shopping malls publish their store directories, but they publish them in eight different ways: one operator runs a headless CMS, another a WordPress store list, another a Laravel JSON blob. Nobody publishes the whole country. So I built an actor that crawls every major operator and emits every store as one consistent record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;If you want the store mix of every mall in Romania — for retail analytics, site selection, or leasing research — you'd normally stitch together NEPI's site, AFI's site, Iulius's site, and so on, and still end up with per-operator formats that don't join. Worse, nobody flags the anchor tenants: the hypermarkets, cinemas and department stores that actually drive foot traffic. That's the gap: a country-wide directory with one schema and anchor flags built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sources
&lt;/h2&gt;

&lt;p&gt;Each operator publishes its own public store data, and each needed a different reverse-engineering pass:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NEPI Rockcastle&lt;/strong&gt; (19 centres, ~2,434 tenants) — their own Strapi CMS JSON API (&lt;code&gt;cms.&amp;lt;site&amp;gt;/api/tenants&lt;/code&gt;), including Mega Mall's 215 stores and Promenada's 182&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AFI Europe&lt;/strong&gt; (Cotroceni, Ploiești) — the &lt;code&gt;evx_retailers&lt;/code&gt; sitemap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iulius Group&lt;/strong&gt; (Cluj, Iași, Suceava, Iulius Town) — a shopping index mixing category and store links, disambiguated by nesting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sun Plaza, Veranda, Colosseum&lt;/strong&gt; — WordPress store sitemaps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Băneasa&lt;/strong&gt; — a Laravel/Inertia &lt;code&gt;data-page&lt;/code&gt; JSON payload across 11 pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The robots.txt lesson deserves a callout: fetching it with Python's default &lt;code&gt;RobotFileParser&lt;/code&gt; gets you 403'd by Sun Plaza and Veranda's WAFs — empty rules, everything disallowed. Fetching it with requests and a browser User-Agent works, and the actor then honours the real rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;Input &lt;code&gt;{"country":"ro","malls":["all"],"includeAnchor":true}&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTP 201, 3,868 well-formed records in ~86 seconds&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;0 empty store names, 0 malformed key sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;382 anchor tenants flagged&lt;/strong&gt;, 2,495 records with floor/unit, 1,499 with phone&lt;/li&gt;
&lt;li&gt;29 of 30 registered malls covered&lt;/li&gt;
&lt;li&gt;All 10 identity fields (mall, store, brand, category, url, …) 100% filled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it again and the &lt;code&gt;first_seen&lt;/code&gt; / &lt;code&gt;last_seen&lt;/code&gt; / &lt;code&gt;is_closed&lt;/code&gt; fields turn the directory into a vacancy and churn time-series — which stores opened, which closed, which anchors moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ParkLake is WAF-gapped from the cloud.&lt;/strong&gt; Sonae Sierra serves an HTTP 202 JavaScript-challenge page to datacenter IPs for its API, sitemap and archive alike; even curl_cffi impersonation hits the wall. From a residential or proxy IP the REST API returns ~223 stores with full detail. The adapter tries hard, then logs the challenge honestly instead of faking data — attach a residential proxy if you need ParkLake from the cloud.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_closed&lt;/code&gt; only becomes meaningful from the &lt;strong&gt;second&lt;/strong&gt; run — the first run is your baseline.&lt;/li&gt;
&lt;li&gt;Fields an operator doesn't publish come back as empty strings, so exports stay clean but sometimes thin.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/ro-mall-store-directory" rel="noopener noreferrer"&gt;&lt;strong&gt;Romania Mall Store Directory on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>data</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Scraping Romanian Public Contracts: A Native-Language Tender &amp; Awarded Deals Scraper</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:30:10 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob</guid>
      <description>&lt;p&gt;Romania publishes every public tender and awarded contract through e-licitatie.ro — the successor to the old SEAP system. The data is public, the opportunities are real (millions of RON in awards every month), and almost nobody outside Romania is building on it. Being native-language gives us an edge no English-first competitor has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The niche's existing actors are thin: the top reference has single-digit runs. Tenders come with deadlines and award values, but most scrapers stop at the listing page. Ours goes deeper — tenders &lt;strong&gt;and&lt;/strong&gt; awarded contracts, with detail enrichment, CPV classification, and winner + CUI (company ID) extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source
&lt;/h2&gt;

&lt;p&gt;The Angular SPA at e-licitatie.ro calls its own JSON API — but it 403s without the right headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST api-pub/NoticeCommon/GetCNoticeListFiltered/   # tenders
POST api-pub/NoticeCommon/GetCANoticeList/          # awarded contracts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick: the API requires a &lt;code&gt;Referer&lt;/code&gt; of &lt;code&gt;https://e-licitatie.ro/pub&lt;/code&gt; plus &lt;code&gt;Origin&lt;/code&gt; and &lt;code&gt;X-Requested-With&lt;/code&gt;. Missing referer, instant 403 "Access Denied: Referrer cannot be null". Detail enrichment needs &lt;code&gt;GetSection21View&lt;/code&gt; (description, CPV, estimated value — keyed off the item's own &lt;code&gt;sysNoticeTypeId&lt;/code&gt;, not the noticeId) and &lt;code&gt;GetSection1View&lt;/code&gt; for the contracting authority. Winners come from &lt;code&gt;GetCANoticeContracts&lt;/code&gt; — and its filter payload is strict: a bare payload 400s.&lt;/p&gt;

&lt;h2&gt;
  
  
  The native-language advantage
&lt;/h2&gt;

&lt;p&gt;Romanian titles, Romanian descriptions, CPV codes joined to EU TED. The reference actors are English-first and miss the nuance — e.g., distinguishing &lt;em&gt;licitație deschisă&lt;/em&gt; (open tender) from &lt;em&gt;achiziție directă&lt;/em&gt; (direct award). Our output carries the original Romanian fields untouched, so downstream users can do their own analysis without a translation layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenders&lt;/strong&gt;: keyword &lt;code&gt;infrastructura&lt;/code&gt;, dateFrom 2026-01-01 → &lt;strong&gt;5 items, 5/5 complete&lt;/strong&gt; — real titles, authorities, CPV names, estimated RON values, deadlines, status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Awarded&lt;/strong&gt;: keyword &lt;code&gt;drumuri&lt;/code&gt;, dateFrom 2026-06-01 → &lt;strong&gt;4 items&lt;/strong&gt;, including winner &lt;strong&gt;ROUTE CENTER CONSTRUCT (CUI 29170569)&lt;/strong&gt; with a contract value of &lt;strong&gt;4,822,401.88 RON&lt;/strong&gt; and an award date&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Authority filtering is client-side containment — the API ignores authority text server-side&lt;/li&gt;
&lt;li&gt;Some CAN (award) records show zero value and no winner — that's data-side, the tender was annulled&lt;/li&gt;
&lt;li&gt;Publication dates are what the portal publishes; actual contract signatures can lag&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/ro-public-contracts-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;Romanian Public Contracts Scraper on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b"&gt;Building a 12-City US Building Permits Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a 12-City US Building Permits Scraper With Python</title>
      <dc:creator>Oaida Adrian</dc:creator>
      <pubDate>Sun, 16 Aug 2026 19:30:09 +0000</pubDate>
      <link>https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b</link>
      <guid>https://dev.to/darksider4all_afa2428f63d0/building-a-12-city-us-building-permits-scraper-with-python-138b</guid>
      <description>&lt;p&gt;Building permits are the earliest public signal of construction activity — new housing, commercial builds, renovation waves. Every major US city publishes them, but each through a different portal with a different schema. I built one scraper that covers twelve of them with a single output shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The reference actor in this niche covers the big cities but has blind spots: in its own all-cities run it scanned &lt;strong&gt;zero&lt;/strong&gt; rows for Washington DC and never even attempted West Sacramento. Ours covers all twelve: ten Socrata portals (Chicago, NYC, LA, San Francisco, Austin, Seattle, Cincinnati, Mesa, Montgomery County, Baton Rouge) plus two ArcGIS servers (DC, West Sacramento).&lt;/p&gt;

&lt;h2&gt;
  
  
  The reverse-engineering
&lt;/h2&gt;

&lt;p&gt;Two very different API families to reconcile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Socrata&lt;/strong&gt; — the well-documented &lt;code&gt;resource&lt;/code&gt; endpoint family, clean JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ArcGIS&lt;/strong&gt; — the old FeatureServer contract, where field names vary per server and some servers reject modern ordering parameters outright&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resilience ladder for the old ArcGIS servers: try &lt;code&gt;TIMESTAMP&lt;/code&gt; + &lt;code&gt;orderBy&lt;/code&gt; first, fall back to &lt;code&gt;where&lt;/code&gt;-only queries, then to unfiltered pulls with client-side date filtering. West Sacramento's server rejects &lt;code&gt;TIMESTAMP&lt;/code&gt; entirely — the ladder is what makes it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  One schema to rule them all
&lt;/h2&gt;

&lt;p&gt;Eighteen canonical keys: city, permit_number, permit_type, status, issue_date, application_date, address, work_description, valuation, contractor_name, applicant, latitude, longitude, source, source_type, source_url, scraped_at, cityKey. NYC's "Permit is not yet issued" placeholder rows get filtered. Adding a city is one entry in the config table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smoke test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;24/24 non-zero&lt;/strong&gt; — two runs per city across all twelve, full 18-key schema on every item, plus local unit tests covering ten Socrata normalisations, both ArcGIS servers, and live fetches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Coverage is US-only and city-scoped — counties and rural areas aren't in this pass&lt;/li&gt;
&lt;li&gt;Valuation can be null on older records; some cities simply don't publish it&lt;/li&gt;
&lt;li&gt;ArcGIS servers rate-limit; the ladder paces requests per city&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://apify.com/darknezz/building-permits-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;Building Permits Scraper on Apify Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More from me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-now-callable-from-claude-and-cursor-mcp-rest-4f3f"&gt;Romania's company registry, now callable from Claude and Cursor (MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/romanias-company-registry-has-no-free-api-so-i-built-one-42m-firms-mcp-rest-234l"&gt;Romania's company registry has no free API — so I built one (4.2M firms, MCP + REST)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romanian-business-registry-api-42m-companies-as-queryable-json-1dh3"&gt;Building a Romanian Business Registry API: 4.2M Companies as Queryable JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-sec-edgar-filings-scraper-10-k-risk-factors-and-13f-holdings-as-clean-json-1paj"&gt;Building a SEC EDGAR Filings Scraper: 10-K Risk Factors and 13F Holdings as Clean JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-price-competitor-monitor-that-diffs-shopify-and-aliexpress-prices-519l"&gt;Building a Price &amp;amp; Competitor Monitor That Diffs Shopify and AliExpress Prices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-vertical-corpus-builder-clean-jsonl-datasets-for-llm-fine-tuning-2p32"&gt;Building a Vertical Corpus Builder: Clean JSONL Datasets for LLM Fine-Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-change-alert-engine-that-webhooks-any-diff-on-any-url-44ip"&gt;Building a Change &amp;amp; Alert Engine That Webhooks Any Diff on Any URL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-romania-mall-store-directory-every-store-in-29-malls-one-schema-2fhb"&gt;Building a Romania Mall Store Directory: Every Store in 29 Malls, One Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-romanian-public-contracts-a-native-language-tender-awarded-deals-scraper-4lob"&gt;Scraping Romanian Public Contracts: A Native-Language Tender &amp;amp; Awarded Deals Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-eu-safety-gate-rapex-product-recall-scraper-with-python-2m26"&gt;Building an EU Safety Gate (RAPEX) Product Recall Scraper With Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-water-utility-risk-intelligence-tool-with-python-and-mcp-1hj0"&gt;How I Built a Water Utility Risk Intelligence Tool With Python and MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-aviation-hub-api-airports-airlines-live-flights-weather-from-six-keyless-sources-58j9"&gt;Building an Aviation Hub API: Airports, Airlines, Live Flights &amp;amp; Weather From Six Keyless Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf"&gt;I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-canada-product-recalls-safety-alerts-scraper-that-reads-open-government-data-3p59"&gt;I Built a Canada Product Recalls &amp;amp; Safety Alerts Scraper That Reads Open Government Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-a-telegram-members-scraper-that-reads-public-chat-stats-without-login-34mc"&gt;I Built a Telegram Members Scraper That Reads Public Chat Stats Without Login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-whois-dns-lookup-tool-domain-intelligence-in-one-api-call-2n5b"&gt;Building a WHOIS &amp;amp; DNS Lookup Tool: Domain Intelligence in One...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-an-ai-web-crawler-that-outputs-llm-ready-content-chunks-4ghg"&gt;Building an AI Web Crawler That Outputs LLM-Ready Content Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-real-time-press-release-monitor-with-python-and-rss-aggregation-76l"&gt;Building a Real-Time Press Release Monitor with Python and RSS...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/building-a-universal-property-listing-scraper-with-python-and-json-ld-3mdj"&gt;Building a Universal Property Listing Scraper with Python and ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/tracking-tech-sentiment-in-real-time-with-vader-and-python-3adl"&gt;Tracking Tech Sentiment in Real-Time with VADER and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-i-built-a-product-hunt-scraper-that-tracks-launches-in-real-time-jkn"&gt;How I Built a Product Hunt Scraper That Tracks Launches in Rea...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/5-apis-every-developer-needs-for-content-processing-rss-extraction-sitemaps-ai-2630"&gt;5 APIs Every Developer Needs for Content Processing (RSS, Extraction, Sitemaps, AI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/how-to-extract-clean-content-from-any-website-sitemap-for-seo-audits-ai-training-15a9"&gt;How to Extract Clean Content From Any Website Sitemap (For SEO...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/scraping-187000-romanian-businesses-building-a-b2b-lead-generation-tool-176n"&gt;Scraping 187,000 Romanian Businesses: Building a B2B Lead Gene...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/make-any-website-ai-readable-generating-llmstxt-files-with-python-3jop"&gt;Make Any Website AI-Readable: Generating llms.txt Files with&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/darksider4all_afa2428f63d0/i-built-an-rss-aggregator-that-extracts-full-article-content-not-just-summaries-ifl"&gt;I Built an RSS Aggregator That Extracts Full Article Content (...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
