<?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: Ken-Mutisya</title>
    <description>The latest articles on DEV Community by Ken-Mutisya (@scrapemint).</description>
    <link>https://dev.to/scrapemint</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%2F4006428%2F450d271d-24fa-4dcd-b75c-cda1fe0182dc.jpg</url>
      <title>DEV Community: Ken-Mutisya</title>
      <link>https://dev.to/scrapemint</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scrapemint"/>
    <language>en</language>
    <item>
      <title>Two public directories that are quietly great B2B lead sources</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Wed, 15 Jul 2026 19:41:43 +0000</pubDate>
      <link>https://dev.to/scrapemint/two-public-directories-that-are-quietly-great-b2b-lead-sources-5d1m</link>
      <guid>https://dev.to/scrapemint/two-public-directories-that-are-quietly-great-b2b-lead-sources-5d1m</guid>
      <description>&lt;p&gt;Most teams buy lead lists. The lists are stale, half the emails bounce, and everyone in your niche bought the same file. Meanwhile some of the best lead sources sit in plain sight: public directories that publish structure, freshness signals and even contact routes, free to read for anyone willing to parse them.&lt;/p&gt;

&lt;p&gt;I run a fleet of scrapers on Apify (a new one ships every few days, so whatever count I write here is already wrong) and the two directory actors I promoted this week are a good case study in what public data can do for outreach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source one: the Y Combinator startup directory.&lt;/strong&gt; YC publishes every funded company with batch, industry, region, team size and hiring status. That is a prefiltered universe of funded buyers: each company passed a selective screen, raised money recently and is usually growing. The directory itself does not list emails, but almost every startup site does, so the actor filters the directory first and then visits each company website to scrape a contact address. One run returns rows like company, pitch, website, team size, batch and the email it found, with the rows that lack an email flagged honestly. Recruiters filter by hiring status, sellers filter by industry, investors watch specific batches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source two: podcast RSS feeds.&lt;/strong&gt; Every podcast feed carries an owner field, and that field usually holds the exact email the host wants business mail sent to. Search the public podcast directory for a keyword, pull each show feed, read the owner email plus episode count and last episode date, and you have a list of active shows in a niche with reachable hosts. The freshness signal matters as much as the email: a show that has not published in two years wastes your pitch, so the actor surfaces the last episode date and lets you skip dead shows before writing to anyone.&lt;/p&gt;

&lt;p&gt;The pattern behind both is the same and it generalizes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find a directory that curates for you. Curation is the expensive part of lead generation, and YC (funded companies) or a podcast index (shows by topic) already did it.&lt;/li&gt;
&lt;li&gt;Look for the freshness field. Hiring status, last episode date, latest batch: recency separates a lead from a museum piece.&lt;/li&gt;
&lt;li&gt;Follow the entity to its own property for contact data. Company sites and RSS feeds publish contact routes on purpose, which makes them fair and reliable in a way harvested emails are not.&lt;/li&gt;
&lt;li&gt;Return misses honestly. A row that says no email found is information, not failure, and it keeps your input list reconciled one to one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything here is keyless HTTP against public endpoints, no browser automation and no login walls, which keeps runs fast and costs tiny. On pay per event pricing that matters: a lead costs a few cents only when the row actually delivers.&lt;/p&gt;

&lt;p&gt;Both actors are live on Apify under the Scrapemint account as YC Startup Leads and Podcast Host Leads, priced per result with the first rows of every run free, and they sit alongside the rest of the fleet: an email checker, a phone checker, a VAT checker and more list hygiene tools that clean whatever the lead sources produce.&lt;/p&gt;

&lt;p&gt;The bigger takeaway is that directories are underrated infrastructure. Everyone scrapes search results and social feeds, which are hostile and noisy, while directories are structured, consented and refreshed by their owners. What public directory in your niche is sitting there unparsed?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>api</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>"Turning Google Maps Into a Clean Local Lead List, With Emails"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:37:39 +0000</pubDate>
      <link>https://dev.to/scrapemint/turning-google-maps-into-a-clean-local-lead-list-with-emails-2la</link>
      <guid>https://dev.to/scrapemint/turning-google-maps-into-a-clean-local-lead-list-with-emails-2la</guid>
      <description>&lt;p&gt;Every local sales motion starts with the same question: who are all the businesses of type X in city Y, and how do I reach them. Google Maps already knows the answer. The trick is getting it out as structured rows instead of a page you scroll forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one place actually contains
&lt;/h2&gt;

&lt;p&gt;A single Google Maps place is far richer than the pin suggests. For each business you can pull:&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;"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;"Bright Smile Dental"&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;"123 Main St, Austin, TX 78701"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+1 512 555 0100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"website"&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://brightsmileaustin.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviews"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;312&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hours"&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;"mon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9-5"&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;"priceRange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;Two things make this a lead list rather than a directory dump. First, the phone and website are on almost every business, so the rows are actually reachable. Second, the rating and review count let you rank by traction, so you call the busy, established places first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing piece: email
&lt;/h2&gt;

&lt;p&gt;Maps gives you a website but not an email, and email is what most outreach tools want. So the last step is a light enrichment pass: take the website from each place, fetch the home and contact pages, and pull the first address plus any obvious contact name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9._%+-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9.-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]{2,}&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that only for places that have a website and you convert a map of pins into a spreadsheet of name, phone, website, email, rating, and reviews, ready to import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query or URL in, rows out
&lt;/h2&gt;

&lt;p&gt;You drive it two ways: a search string like "dentists in Austin" to sweep a whole category in a city, or a specific place URL when you already know the business. Either way you get one clean row per place, and the enrichment is optional so you only pay for emails when you want them.&lt;/p&gt;

&lt;p&gt;I packaged the whole flow, Maps parsing plus the website email enrichment, here: &lt;a href="https://apify.com/scrapemint/google-maps-scraper" rel="noopener noreferrer"&gt;https://apify.com/scrapemint/google-maps-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And counting. It is one of a set of lead tools I have been writing up one at a time.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>api</category>
    </item>
    <item>
      <title>"The Shopify App Store Hands You a B2B Lead List, for Free"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:37:02 +0000</pubDate>
      <link>https://dev.to/scrapemint/the-shopify-app-store-hands-you-a-b2b-lead-list-for-free-fl</link>
      <guid>https://dev.to/scrapemint/the-shopify-app-store-hands-you-a-b2b-lead-list-for-free-fl</guid>
      <description>&lt;p&gt;Shopify app developers are a captive market: agencies, tooling makers, and app store optimization services all want to reach them. It turns out the Shopify App Store publishes everything you need to build that lead list, no login and no API key, if you know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: the sitemap gives you every app handle
&lt;/h2&gt;

&lt;p&gt;The App Store ships an XML sitemap. Fetch it and you get the canonical handle for every listed app.&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;GET https://apps.shopify.com/sitemap.xml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It points at child sitemaps, each of which lists app URLs like &lt;code&gt;https://apps.shopify.com/klaviyo-email-marketing&lt;/code&gt;. Pull the last path segment and you have a stable id for every app on the platform. No pagination games, no scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: each listing embeds the developer contact
&lt;/h2&gt;

&lt;p&gt;Open any app detail page and the developer contact is right there in the HTML, in a data attribute the page uses for its own "contact" button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data-developer-support-email="support@example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Shopify app is required to publish a support email, so coverage is effectively total. Alongside it you get the developer name, the partner page, the company website, the rating, the review count, and the pricing tiers, all in the server rendered markup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://apps.shopify.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/data-developer-support-email="&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/&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="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: rank by traction
&lt;/h2&gt;

&lt;p&gt;Because you also get rating and review counts, you can sort the whole list by how much traction each app has, so outreach starts with the developers who are clearly making money and reinvesting in tooling.&lt;/p&gt;

&lt;p&gt;That is the whole recipe: one sitemap for the universe of apps, one regex per detail page for the reachable contact, and a sort by reviews. No keys, no proxy, no browser.&lt;/p&gt;

&lt;p&gt;I packaged it as a keyword search so you can pull, say, every email marketing or upsell app maker with a public support email in one run. It lives here: &lt;a href="https://apify.com/scrapemint/shopify-app-developer-leads" rel="noopener noreferrer"&gt;https://apify.com/scrapemint/shopify-app-developer-leads&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And counting. This is one of a batch of keyless lead sources I have been documenting one at a time.&lt;/p&gt;

</description>
      <category>api</category>
      <category>node</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Every Federal Contract Award Is a Free JSON Row (GovCon Tools Charge $500/mo for This)"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Fri, 10 Jul 2026 14:12:16 +0000</pubDate>
      <link>https://dev.to/scrapemint/every-federal-contract-award-is-a-free-json-row-govcon-tools-charge-500mo-for-this-21kk</link>
      <guid>https://dev.to/scrapemint/every-federal-contract-award-is-a-free-json-row-govcon-tools-charge-500mo-for-this-21kk</guid>
      <description>&lt;p&gt;GovCon market intelligence is one of the priciest alert categories in B2B SaaS: GovWin, HigherGov and friends run from hundreds to thousands per month. The primary source under all of them is USAspending.gov, and its API is public JSON with no key, no registration, and no meaningful rate limits. Here is the part that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search is one POST
&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;POST https://api.usaspending.gov/api/v2/search/spending_by_award/
Content-Type: application/json

{
  "filters": {
    "time_period": [{
      "start_date": "2026-07-03",
      "end_date": "2026-07-10",
      "date_type": "date_signed"
    }],
    "award_type_codes": ["A", "B", "C", "D"],
    "keywords": ["software"],
    "award_amounts": [{ "lower_bound": 250000 }]
  },
  "fields": ["Award ID", "Recipient Name", "Award Amount", "Description",
             "Awarding Agency", "NAICS", "PSC", "recipient_id"],
  "limit": 100, "page": 1,
  "sort": "Award Amount", "order": "desc"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filters compose: &lt;code&gt;naics_codes&lt;/code&gt; (prefixes work, 54 = all professional services), &lt;code&gt;agencies&lt;/code&gt; by name, &lt;code&gt;recipient_type_names&lt;/code&gt; (small_business, veteran owned...). Two gotchas: the &lt;code&gt;sort&lt;/code&gt; field must appear in &lt;code&gt;fields&lt;/code&gt; or you get a 400, and keyword search can take 30+ seconds cold, so set your timeout accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  date_signed is the whole product
&lt;/h2&gt;

&lt;p&gt;The default date filter, &lt;code&gt;action_date&lt;/code&gt;, matches every modification to every old contract: option years, funding bumps, admin changes. Alert products built on it are noise. &lt;code&gt;date_type: "date_signed"&lt;/code&gt; returns contracts that were actually signed in the window, which is the event people pay to hear about. A company that signed a $5M federal contract this week is hiring against a start date, buying equipment, and looking for subcontractors right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recipient profile is the lead
&lt;/h2&gt;

&lt;p&gt;Each result carries a &lt;code&gt;recipient_id&lt;/code&gt;. One more GET turns the award into an addressable company:&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;GET https://api.usaspending.gov/api/v2/recipient/{recipient_id}/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the registered street address, UEI, business type flags (small business, veteran owned, minority owned, ...) and lifetime prime award totals. The lifetime number is a free qualifier: a first time winner with a big award is setting up everything at once, while a $1B incumbent is a different sales conversation entirely. The endpoint is transiently flaky under concurrency, so retry once before giving up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What registries this beats
&lt;/h2&gt;

&lt;p&gt;SAM.gov has the richer entity data but requires an API key and an account. The FPDS ATOM feed is XML from 2007. USAspending is the only corner of the federal procurement stack that is modern JSON with zero signup, and it updates within days of signing.&lt;/p&gt;

&lt;p&gt;I packaged the whole flow (keyword/NAICS/agency filters, date_signed windows, recipient enrichment, retry logic) into a pay per row actor: &lt;a href="https://apify.com/scrapemint/government-contract-winner-leads" rel="noopener noreferrer"&gt;Government Contract Winner Leads&lt;/a&gt;. It is free to try until July 24. And if you would rather build your own alert, the two requests above are the entire pipeline.&lt;/p&gt;

</description>
      <category>api</category>
      <category>node</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Stop Scraping WHOIS. The Registries Serve JSON Now"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Fri, 10 Jul 2026 13:46:50 +0000</pubDate>
      <link>https://dev.to/scrapemint/stop-scraping-whois-the-registries-serve-json-now-4e59</link>
      <guid>https://dev.to/scrapemint/stop-scraping-whois-the-registries-serve-json-now-4e59</guid>
      <description>&lt;p&gt;Every few months someone asks how to scrape WHOIS at scale, and the answers are always the same: port 43 sockets, regex soup for 200 registrar formats, and rate limits that ban you by lunchtime. Almost nobody mentions that the registries replaced the whole thing years ago with a JSON protocol called RDAP, and it is open, keyless, and standardized.&lt;/p&gt;

&lt;h2&gt;
  
  
  One GET, structured JSON
&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://rdap.verisign.com/com/v1/domain/stripe.com
Accept: application/rdap+json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get events (registration, expiration, last changed), status locks, nameservers, DNSSEC delegation, and the registrar as a structured entity. No text parsing. The dates are ISO 8601. The response for a .com domain comes straight from Verisign, the registry itself, not a reseller or a cached mirror.&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;"events"&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;span class="nl"&gt;"eventAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"eventDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1995-09-12T04:00:00Z"&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="nl"&gt;"eventAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expiration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"eventDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-09-11T04:00:00Z"&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="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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"client delete prohibited"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client transfer prohibited"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nameservers"&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;"ldhName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NS1.STRIPE.COM"&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;h2&gt;
  
  
  Routing: the IANA bootstrap file
&lt;/h2&gt;

&lt;p&gt;Different TLDs live at different registries, and IANA publishes the routing table as plain JSON:&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;GET https://data.iana.org/rdap/dns.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It maps about 1,200 TLDs to their RDAP base URLs. Fetch it once, build a &lt;code&gt;Map&lt;/code&gt; of TLD to endpoint, and query each domain at its own registry. Concurrency 8 with a 15 second timeout checks 2,000 domains in a couple of minutes, and nobody rate walls you, because you are spreading standard queries across the services built to answer them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://data.iana.org/rdap/dns.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tlds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tld&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tlds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tld&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 404 is a feature
&lt;/h2&gt;

&lt;p&gt;An RDAP 404 from the authoritative registry means the domain is not registered. That turns a registration data checker into a bulk availability checker for free. But only trust the 404 when it comes from the TLD's own registry. Aggregators like rdap.org return 404 both for "domain not found" and "TLD not supported", which will happily tell you github.io is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gaps are ccTLDs
&lt;/h2&gt;

&lt;p&gt;ICANN requires RDAP for every gTLD, so .com, .net, .org and the hundreds of new TLDs have full coverage. Country TLDs are voluntary: .ai, .tv, .cc and .uk are in the bootstrap, while .io and .sh are missing from it but quietly served by the registry operator at &lt;code&gt;rdap.identitydigital.services/rdap/&lt;/code&gt;. Others (.de, .co, .me) still have nothing public. Handle the miss case explicitly instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing the registrar
&lt;/h2&gt;

&lt;p&gt;The registrar hides inside a jCard, which is vCard reimagined as nested JSON arrays. The name is the &lt;code&gt;fn&lt;/code&gt; entry, and the IANA registrar ID is in &lt;code&gt;publicIds&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registrar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;vcardArray&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="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One postscript on privacy: registrant name and email are redacted nearly everywhere since GDPR, in RDAP and WHOIS alike. What registries still publish for every domain is the part that matters for most workflows anyway: dates, registrar, locks, nameservers. Domain age alone sorts a lead list by business credibility, flags a phishing lookalike registered last Tuesday, and prices an expired domain hunt.&lt;/p&gt;

&lt;p&gt;I packaged this whole flow (bootstrap routing, ccTLD supplements, jCard parsing, availability detection) into a pay per row actor: &lt;a href="https://apify.com/scrapemint/domain-whois-checker" rel="noopener noreferrer"&gt;Domain WHOIS &amp;amp; Age Checker&lt;/a&gt;. Paste domains, get rows. It is free to try until July 24. But if you would rather build it yourself, everything above is the entire recipe.&lt;/p&gt;

</description>
      <category>api</category>
      <category>node</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Every US Nonprofit's Budget Is Public. Here Is How to Build Lead Lists From It"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Tue, 07 Jul 2026 19:35:58 +0000</pubDate>
      <link>https://dev.to/scrapemint/every-us-nonprofits-budget-is-public-here-is-how-to-build-lead-lists-from-it-bbg</link>
      <guid>https://dev.to/scrapemint/every-us-nonprofits-budget-is-public-here-is-how-to-build-lead-lists-from-it-bbg</guid>
      <description>&lt;p&gt;If you sell anything to nonprofits — software, fundraising services, consulting — you have one segmentation question that matters more than all others: &lt;em&gt;can this organization afford me?&lt;/em&gt; Unusually, the answer is public. Every US nonprofit above a minimal size files a Form 990 with the IRS, and the filings carry total revenue, expenses, and assets.&lt;/p&gt;

&lt;p&gt;The friendly way in is ProPublica's Nonprofit Explorer API. No key, no registration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search, then enrich
&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://projects.propublica.org/nonprofits/api/v2/search.json
      ?q=food+bank&amp;amp;state[id]=TX&amp;amp;ntee[id]=5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns organizations with EIN, name, city, state, and NTEE category (1=Arts through 9=Mutual Benefit), paginated 25 at a time. Then per organization:&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;GET https://projects.propublica.org/nonprofits/api/v2/organizations/{ein}.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;filings_with_data&lt;/code&gt; array is the payoff — one entry per e-filed 990, each with &lt;code&gt;tax_prd_yr&lt;/code&gt;, &lt;code&gt;totrevenue&lt;/code&gt;, &lt;code&gt;totfuncexpns&lt;/code&gt;, &lt;code&gt;totassetsend&lt;/code&gt;, and a link to the filing PDF. Sort by year descending and you have the latest financials plus a multi-year trend for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three moves that turn data into leads
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Filter by revenue band.&lt;/strong&gt; &lt;code&gt;totalRevenue &amp;gt;= 1_000_000&lt;/code&gt; removes the all-volunteer organizations that can't buy anything. This single filter is most of what expensive sector databases sell — GuideStar/Candid Pro starts around $2k/year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the trend, not the snapshot.&lt;/strong&gt; Five years of &lt;code&gt;totrevenue&lt;/code&gt; distinguishes a growing organization (hiring, buying tools) from a shrinking one (cutting). &lt;code&gt;{"2023": 401M, "2022": 388M, "2021": 421M}&lt;/code&gt; tells you more than any firmographic field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat missing filings as signal, not noise.&lt;/strong&gt; Small organizations file postcard 990-Ns with no financial data. If your product needs a real budget behind it, the absence of filing data &lt;em&gt;is&lt;/em&gt; the disqualification — skip them (and if you're billing per row like I do, make them free).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Search fan-out (terms × states × categories) → dedupe by EIN → enrich → filter by band → export. Add a store of seen EINs and a weekly schedule and you get only &lt;em&gt;new&lt;/em&gt; organizations matching your profile each run — a prospecting feed instead of a static list. Chain the names into a website contact scraper and the outreach email is one more step.&lt;/p&gt;

&lt;p&gt;I packaged this as an Apify actor: &lt;a href="https://apify.com/scrapemint/nonprofit-leads-scraper" rel="noopener noreferrer"&gt;Nonprofit Leads Scraper&lt;/a&gt; — search filters, 990 enrichment with the five-year trend, revenue-band filtering, cross-run dedupe, at $0.01 per organization row (no-filing orgs free, first 2 rows of every run free). It pairs with the &lt;a href="https://apify.com/scrapemint/grant-opportunity-finder" rel="noopener noreferrer"&gt;Grant Opportunity Finder&lt;/a&gt; I shipped the same week: one side of the sector's money graph is who gives, the other is who raises and spends.&lt;/p&gt;

&lt;p&gt;The nonprofit sector runs on transparency rules the for-profit world would never accept. If your ideal customer is a nonprofit, their budget has been sitting in a public API the whole time.&lt;/p&gt;

</description>
      <category>api</category>
      <category>data</category>
      <category>rpa</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"The $459/mo Grant Alert Industry Runs on a Free Government API"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:00:19 +0000</pubDate>
      <link>https://dev.to/scrapemint/the-459mo-grant-alert-industry-runs-on-a-free-government-api-1kdp</link>
      <guid>https://dev.to/scrapemint/the-459mo-grant-alert-industry-runs-on-a-free-government-api-1kdp</guid>
      <description>&lt;p&gt;Grant discovery platforms are a quiet SaaS category with remarkable pricing: $179 to $459 a month for keyword alerts on grant opportunities. The primary source for US federal grants is grants.gov, and it has a public JSON API with no key, no registration, and no rate-limit drama. Here is the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search is one POST
&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;POST https://api.grants.gov/v1/api/search2
Content-Type: application/json

{
  "keyword": "rural health",
  "oppStatuses": "posted|forecasted",
  "eligibilities": "12",
  "rows": 100,
  "startRecordNum": 0
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get &lt;code&gt;hitCount&lt;/code&gt; and an &lt;code&gt;oppHits&lt;/code&gt; array: opportunity ID, number, title, agency code, open/close dates, status, and CFDA numbers. Filters compose with pipes: &lt;code&gt;agencies&lt;/code&gt; (NSF, HHS, USDA...), &lt;code&gt;fundingCategories&lt;/code&gt; (ED, ENV, HL...), &lt;code&gt;eligibilities&lt;/code&gt; (12 = 501(c)(3) nonprofits, 20 = private universities, 22 = for-profits). Pagination is &lt;code&gt;startRecordNum&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The status values matter more than they look. &lt;code&gt;posted&lt;/code&gt; is open-now. &lt;code&gt;forecasted&lt;/code&gt; is the interesting one: grants that are &lt;em&gt;announced but not yet open&lt;/em&gt; — the window where a grant writer positions a client before the competition starts writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Details are one more POST
&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;POST https://api.grants.gov/v1/api/fetchOpportunity
{"opportunityId": 103313}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The synopsis block carries what the alert platforms sell as premium fields: &lt;code&gt;awardCeiling&lt;/code&gt;, &lt;code&gt;awardFloor&lt;/code&gt;, &lt;code&gt;estimatedFunding&lt;/code&gt;, &lt;code&gt;expectedNumberOfAwards&lt;/code&gt;, &lt;code&gt;applicantTypes&lt;/code&gt;, &lt;code&gt;costSharing&lt;/code&gt;, the full description, and — the field that surprised me — &lt;code&gt;agencyContactEmail&lt;/code&gt;, the actual program officer inbox. In my verification run, 22 of 25 opportunities had one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two gotchas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Relevance order lies.&lt;/strong&gt; The default ordering happily surfaces a "posted" opportunity from 2012. Sort client-side by &lt;code&gt;openDate&lt;/code&gt; descending and drop anything whose &lt;code&gt;closeDate&lt;/code&gt; is behind you, or your "new grants" feed opens with the Obama administration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money fields are strings with moods.&lt;/strong&gt; &lt;code&gt;awardCeiling&lt;/code&gt; can be a number, an empty string, or the literal string &lt;code&gt;"none"&lt;/code&gt;. Normalize before you compare.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Alerts are dedupe plus a schedule
&lt;/h2&gt;

&lt;p&gt;The product these platforms sell is "tell me only what is new for my profile." That is: store the opportunity IDs you have already returned (a named key-value store on Apify), run the search on a weekly schedule, and skip seen IDs. Twenty lines of code, and the output is rows you can pipe to Slack or a client tracker instead of a marketing email.&lt;/p&gt;

&lt;p&gt;I packaged all of it as an Apify actor: &lt;a href="https://apify.com/scrapemint/grant-opportunity-finder" rel="noopener noreferrer"&gt;Grant Opportunity Finder&lt;/a&gt; — keywords, agency, category, and eligibility filters, forecasted support, detail enrichment with contact emails, recency sorting, past-deadline filtering, and cross-run dedupe, at $0.01 per opportunity row with the first 2 rows of every run free. The 25-row verification run cost $0.00056 to produce, which tells you everything about the margin structure of the incumbents.&lt;/p&gt;

&lt;p&gt;Public data with a subscription moat around it is the most common shape of the alert-tool economy. Sometimes the moat is real (hard scraping, entity resolution). Here, it is one POST request.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>node</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Apple Publishes Its App Store Charts as a Free JSON Feed. Rank Trackers Charge $40/mo for It"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Tue, 07 Jul 2026 17:44:48 +0000</pubDate>
      <link>https://dev.to/scrapemint/apple-publishes-its-app-store-charts-as-a-free-json-feed-rank-trackers-charge-40mo-for-it-40m</link>
      <guid>https://dev.to/scrapemint/apple-publishes-its-app-store-charts-as-a-free-json-feed-rank-trackers-charge-40mo-for-it-40m</guid>
      <description>&lt;p&gt;Every ASO tool sells the same first feature: "track your app's chart position daily." Here is the part they rarely mention — Apple publishes the charts as a public, keyless JSON feed, and it has worked for over a decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feed
&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://itunes.apple.com/us/rss/topfreeapplications/limit=100/genre=6014/json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Country&lt;/strong&gt;: &lt;code&gt;us&lt;/code&gt;, &lt;code&gt;gb&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;jp&lt;/code&gt;, &lt;code&gt;br&lt;/code&gt; — any storefront code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chart&lt;/strong&gt;: &lt;code&gt;topfreeapplications&lt;/code&gt;, &lt;code&gt;toppaidapplications&lt;/code&gt;, &lt;code&gt;topgrossingapplications&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genre&lt;/strong&gt;: Apple's genre IDs (&lt;code&gt;6014&lt;/code&gt; Games, &lt;code&gt;6015&lt;/code&gt; Finance, &lt;code&gt;6005&lt;/code&gt; Social Networking); drop the segment for the overall chart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth&lt;/strong&gt;: &lt;code&gt;limit=&lt;/code&gt; up to 200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each entry carries the app ID, name, developer, price with currency, category, release date, and artwork URLs. Position in the array is the rank. No key, no auth, no bot wall — this is a feed Apple built to be consumed.&lt;/p&gt;

&lt;p&gt;(There is also a newer &lt;code&gt;rss.marketingtools.apple.com&lt;/code&gt; API, but it dropped genre filtering, and category charts are where the useful signal lives — a niche app can rank #12 in Finance/DE while being invisible in the overall top 200.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What turns a feed into a tracker
&lt;/h2&gt;

&lt;p&gt;The feed is a snapshot. The product is the &lt;em&gt;delta&lt;/em&gt;. Three things to add:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;State between runs.&lt;/strong&gt; Store &lt;code&gt;{appId: rank}&lt;/code&gt; per chart (country × type × genre). On the next run, join against it: &lt;code&gt;previousRank&lt;/code&gt;, &lt;code&gt;rankChange&lt;/code&gt;, and an &lt;code&gt;isNew&lt;/code&gt; flag for chart entries. On Apify this is a named key-value store, so a scheduled run compares automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tracked-apps mode.&lt;/strong&gt; Most buyers care about &lt;em&gt;their&lt;/em&gt; app, not the whole chart. Filter rows to a list of app IDs — and report "not charting" explicitly (as a free row, since no rank is not data you should pay for).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rank direction that reads correctly.&lt;/strong&gt; &lt;code&gt;rankChange = previousRank - rank&lt;/code&gt;, so climbing from #47 to #31 is &lt;code&gt;+16&lt;/code&gt;. Positive equals good news; your Slack alert should not need a legend.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The rising-apps trick
&lt;/h2&gt;

&lt;p&gt;The same rows serve a second buyer. Scan a full category chart daily and filter &lt;code&gt;isNew == true&lt;/code&gt; or &lt;code&gt;rankChange &amp;gt; 20&lt;/code&gt;: that is a rising-apps detector, and it surfaces breakout apps days before the roundup articles, in whatever country you point it at. Publishers and investors pay for exactly this view.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics
&lt;/h2&gt;

&lt;p&gt;One chart is one HTTP request. Pulling 50 ranks costs a few hundredths of a cent in compute, which is why per-row pricing works against $40+/month subscriptions. I packaged the whole thing as an Apify actor this week: &lt;a href="https://apify.com/scrapemint/app-store-top-charts-tracker" rel="noopener noreferrer"&gt;App Store Top Charts Tracker&lt;/a&gt; does the fan-out (countries × charts × categories), the state comparison, and the tracked-apps mode, at $0.003 per rank row with the first 2 rows of every run free.&lt;/p&gt;

&lt;p&gt;Google Play, for the record, has no equivalent public feed — its charts hide behind an internal RPC. That asymmetry says a lot: the data was never the moat. The scheduling, the state, and the diff were, and those are about 100 lines of code.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>api</category>
      <category>ios</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Stop Paying Monthly to Know a Web Page Changed. Diff It Yourself"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Tue, 07 Jul 2026 16:31:14 +0000</pubDate>
      <link>https://dev.to/scrapemint/stop-paying-monthly-to-know-a-web-page-changed-diff-it-yourself-179n</link>
      <guid>https://dev.to/scrapemint/stop-paying-monthly-to-know-a-web-page-changed-diff-it-yourself-179n</guid>
      <description>&lt;p&gt;Change-monitoring SaaS has a strange pricing model: you pay every month for the pages that did &lt;em&gt;not&lt;/em&gt; change. The actual work — fetch, extract, compare — is close to free, and the hard parts are two design decisions most tools get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: what counts as "the page"
&lt;/h2&gt;

&lt;p&gt;Hash the raw HTML and everything is a change: rotating nonces, cache busters, CSRF tokens, ad slots. The signal is in the &lt;em&gt;content layer&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Strip &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;footer&lt;/code&gt;, &lt;code&gt;aside&lt;/code&gt;, cookie banners, and &lt;code&gt;aria-hidden&lt;/code&gt; nodes.&lt;/li&gt;
&lt;li&gt;Scope to &lt;code&gt;main&lt;/code&gt; / &lt;code&gt;article&lt;/code&gt; / &lt;code&gt;[role="main"]&lt;/code&gt; when the page declares one, else &lt;code&gt;body&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Flatten block elements to lines, collapse whitespace per line, drop empties.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hash &lt;em&gt;that&lt;/em&gt;. A pricing page now only "changes" when prices, plans, or copy change — not when the CDN rotates an asset fingerprint. Offer a CSS selector as an override for surgical cases (&lt;code&gt;.pricing-table&lt;/code&gt;), but make the no-selector path the default; nobody wants to maintain selectors for 100 monitored URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: what a "change" looks like in the output
&lt;/h2&gt;

&lt;p&gt;A screenshot pair is where information goes to die. The useful output is a line diff:&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;"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://competitor.com/pricing"&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;"changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"addedLines"&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="s2"&gt;"Pro plan $49/mo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"removedLines"&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="s2"&gt;"Pro plan $39/mo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"previousChangeAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-20T08:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-07T08:00:00Z"&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;You do not need a full LCS diff for this. A multiset comparison of lines (count occurrences per line on each side, report the surplus in each direction) catches everything a human cares about on a monitored page, runs in linear time, and never blows up on a 10,000-line page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state problem
&lt;/h2&gt;

&lt;p&gt;The piece that makes this a product instead of a script is persistence: each URL's last text, hash, and timestamps have to survive between runs. On Apify that is a named key-value store — it lives in your account, so a scheduled run picks up exactly where the last one left off. Key the state by URL &lt;em&gt;plus&lt;/em&gt; selector, so changing the selector re-baselines cleanly instead of producing one giant false diff.&lt;/p&gt;

&lt;p&gt;Baselines deserve their own status. The first time a URL is seen there is nothing to compare against; report &lt;code&gt;baseline&lt;/code&gt;, store the state, charge nothing. A monitoring tool that bills you for learning what a page looks like is charging you for its own setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics
&lt;/h2&gt;

&lt;p&gt;A plain-HTTP check on a monitored page costs a few thousandths of a cent in compute. That is why per-change pricing works: I packaged this as an Apify actor — &lt;a href="https://apify.com/scrapemint/website-change-monitor" rel="noopener noreferrer"&gt;Website Change Monitor&lt;/a&gt; — where baselines, unchanged checks, and fetch errors are free and you pay $0.01 per detected change. A hundred stable pages on a daily schedule cost exactly $0 until one of them moves. The verification run for this article watched Hacker News and example.com: HN came back &lt;code&gt;changed +28/-28&lt;/code&gt;, example.com came back &lt;code&gt;unchanged&lt;/code&gt;, and the bill for both checks was four hundredths of a cent.&lt;/p&gt;

&lt;p&gt;Wire the dataset into Slack or Sheets with an integration and you have the same alerts the subscription tools sell, except the diff is structured, the state is yours, and the invoice only exists when the web actually changed.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Your SEO Audit Should Be JSON, Not a 40-Tab Spreadsheet"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Tue, 07 Jul 2026 15:27:39 +0000</pubDate>
      <link>https://dev.to/scrapemint/your-seo-audit-should-be-json-not-a-40-tab-spreadsheet-kf8</link>
      <guid>https://dev.to/scrapemint/your-seo-audit-should-be-json-not-a-40-tab-spreadsheet-kf8</guid>
      <description>&lt;p&gt;Every agency has the same Monday ritual: open the desktop crawler, wait for it to chew through a client site, export a giant spreadsheet, and copy numbers into a report nobody reads past page two. The data in that workflow is fine. The shape of it is the problem.&lt;/p&gt;

&lt;p&gt;An audit is more useful as one JSON row per page with a computed &lt;code&gt;issues&lt;/code&gt; array. Here is why, and how to build the checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The row, not the report
&lt;/h2&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;"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://example.com/pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"titleLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metaDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"h1Count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wordCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redirectHops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brokenLinkCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&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="s2"&gt;"title_too_short"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing_meta_description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="s2"&gt;"multiple_h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thin_content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"broken_internal_links"&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;Once a page is a row, everything downstream is trivial: filter &lt;code&gt;issueCount &amp;gt; 0&lt;/code&gt;, group by issue type, diff this week against last week, pipe into a dashboard, alert on regressions. The "report" becomes a query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checks worth automating
&lt;/h2&gt;

&lt;p&gt;Most of a technical audit is a handful of deterministic checks per page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title&lt;/strong&gt;: missing, under ~10 chars, over ~60 chars, duplicated across pages. The duplicate check must run across the whole crawl, not per page — keep a &lt;code&gt;Map&lt;/code&gt; of title → first URL and flag later occurrences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta description&lt;/strong&gt;: missing, over ~160 chars, duplicated (same cross-crawl map trick).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headings&lt;/strong&gt;: zero H1s or more than one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexability&lt;/strong&gt;: &lt;code&gt;noindex&lt;/code&gt; in robots meta, missing canonical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect chains&lt;/strong&gt;: follow redirects manually (&lt;code&gt;redirect: 'manual'&lt;/code&gt;) so you can count hops and record each status. A 301 → 302 → 200 chain is invisible if your HTTP client silently follows it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken internal links&lt;/strong&gt;: collect every internal href during the crawl, HEAD-check each unique URL exactly once, cache the status run-wide. Fall back to a 1-byte ranged GET when a server rejects HEAD with 405.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content&lt;/strong&gt;: word count after stripping nav/header/footer/scripts — thin-content thresholds are debatable, but under ~150 words is rarely a page that deserves to rank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;: count how many are missing alt text; don't fail the page, report the number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needs a browser. Marketing sites, blogs, docs, and stores are server-rendered; a plain HTTP fetch plus an HTML parser audits them accurately, and the whole crawl runs in seconds instead of minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduling trick that makes it an agency product
&lt;/h2&gt;

&lt;p&gt;A one-off audit is a snapshot. The valuable thing is the &lt;em&gt;diff&lt;/em&gt;: schedule the crawl weekly per client, store each run's rows, and compare issues arrays. "3 new broken links, 2 pages went noindex after Thursday's deploy" is a report clients act on — and it is 10 lines of comparison code once your audit is rows instead of a spreadsheet.&lt;/p&gt;

&lt;p&gt;I packaged this as an Apify actor this week: &lt;a href="https://apify.com/scrapemint/seo-site-audit-scraper" rel="noopener noreferrer"&gt;SEO Site Audit Scraper&lt;/a&gt; does everything above (BFS crawl with include/exclude filters, sitemap seeding, manual redirect chains, cross-crawl duplicate detection, free HEAD-checked broken links) and returns one row per page, pay per page, no seat license. The first 2 pages of every run are free.&lt;/p&gt;

&lt;p&gt;The desktop crawlers are good tools. But if your audit ends up in a dashboard, a client report, or a CI check, it should have been JSON from the start.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>"Four Remote Job Boards Have Free Public APIs. Here Is One Schema for All of Them"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Sun, 05 Jul 2026 21:52:06 +0000</pubDate>
      <link>https://dev.to/scrapemint/four-remote-job-boards-have-free-public-apis-here-is-one-schema-for-all-of-them-j1f</link>
      <guid>https://dev.to/scrapemint/four-remote-job-boards-have-free-public-apis-here-is-one-schema-for-all-of-them-j1f</guid>
      <description>&lt;p&gt;If you want remote job data, you do not need to scrape HTML or sign up for anything. Four of the bigger remote job boards publish keyless public feeds. The catch is that they all speak different dialects, so the real work is normalization. Here are the endpoints and the traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four feeds
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RemoteOK&lt;/strong&gt; returns its whole current board as one JSON array:&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;GET https://remoteok.com/api
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first element is a legal notice, not a job: they ask for a link back with attribution as a condition of using the feed. Skip element zero, and honor the attribution if you republish. Jobs carry &lt;code&gt;salary_min&lt;/code&gt; and &lt;code&gt;salary_max&lt;/code&gt; as numbers, tags, and ISO dates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remotive&lt;/strong&gt; has the friendliest API of the four, including server side search:&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;GET https://remotive.com/api/remote-jobs?search=python&amp;amp;limit=100
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Salary here is free text (&lt;code&gt;"$120k - $160k"&lt;/code&gt;), so do not expect numbers. Attribution with a link back is required here too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WeWorkRemotely&lt;/strong&gt; publishes RSS:&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;GET https://weworkremotely.com/remote-jobs.rss
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two quirks: the company name is not a field, it is baked into the title as &lt;code&gt;Company: Role&lt;/code&gt;, so split on the first colon. And useful data hides in nonstandard tags like &lt;code&gt;&amp;lt;region&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;skills&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;category&amp;gt;&lt;/code&gt; that generic RSS parsers drop on the floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Himalayas&lt;/strong&gt; has a proper paginated API with a surprisingly deep catalog (100k+ listings):&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;GET https://himalayas.app/jobs/api?limit=100&amp;amp;offset=0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It gives structured &lt;code&gt;minSalary&lt;/code&gt;/&lt;code&gt;maxSalary&lt;/code&gt; with a currency and period, seniority arrays, location restrictions, and even timezone restrictions as UTC offsets. Dates are epoch seconds, not ISO strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The normalization layer
&lt;/h2&gt;

&lt;p&gt;The row schema that survived contact with all four sources:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remotive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Senior Backend Engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"salaryMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"salaryMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"salaryText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$120k - $160k"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Worldwide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-03T20:01:13.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"applyUrl"&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://..."&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;Rules that mattered in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep both salary shapes. Boards with numbers fill &lt;code&gt;salaryMin&lt;/code&gt;/&lt;code&gt;salaryMax&lt;/code&gt;; boards with prose fill &lt;code&gt;salaryText&lt;/code&gt;. Collapsing one into the other loses information either way.&lt;/li&gt;
&lt;li&gt;Normalize every date to ISO 8601 at the edge. Epoch seconds, RFC 822 RSS dates, and ISO strings all flow through one converter, so downstream code never branches on source.&lt;/li&gt;
&lt;li&gt;Dedupe on lowercased &lt;code&gt;title|company&lt;/code&gt;. Companies cross post to multiple boards, and the same listing showing up four times makes the feed look broken.&lt;/li&gt;
&lt;li&gt;Carry &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;sourceUrl&lt;/code&gt; on every row. It satisfies the attribution requirements and it turns out buyers of job data want to know provenance anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this is good for
&lt;/h2&gt;

&lt;p&gt;The obvious build is a job alert pipeline: run it hourly with keywords, diff against what you have seen, push new rows to Slack. The less obvious one is sales intelligence: a company hiring for a role is telling you what they are about to spend money on, and job feeds are the earliest public signal of that.&lt;/p&gt;

&lt;p&gt;I packaged the whole thing (four fetchers, normalization, dedupe, keyword and freshness filters) into &lt;a href="https://apify.com/scrapemint/remote-jobs-scraper" rel="noopener noreferrer"&gt;an actor on Apify&lt;/a&gt; if you want it as a scheduled feed. But every endpoint above works with nothing more than &lt;code&gt;fetch&lt;/code&gt;, and the boards deserve the link backs their terms ask for.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>career</category>
      <category>javascript</category>
    </item>
    <item>
      <title>"arXiv Has One of the Last Truly Open APIs. Here Is How to Build a Paper Monitor on It"</title>
      <dc:creator>Ken-Mutisya</dc:creator>
      <pubDate>Sun, 05 Jul 2026 20:44:14 +0000</pubDate>
      <link>https://dev.to/scrapemint/arxiv-has-one-of-the-last-truly-open-apis-here-is-how-to-build-a-paper-monitor-on-it-27bn</link>
      <guid>https://dev.to/scrapemint/arxiv-has-one-of-the-last-truly-open-apis-here-is-how-to-build-a-paper-monitor-on-it-27bn</guid>
      <description>&lt;p&gt;Every scraping post I write lately is about working around something: bot walls, consent screens, keys that require a developer account. This one is different. arXiv runs one of the last genuinely open APIs on the research web, and it is the right way to keep up with the ~800 AI papers that land there every week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole API is one endpoint
&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://export.arxiv.org/api/query
      ?search_query=(all:"multi-agent") AND (cat:cs.AI)
      &amp;amp;sortBy=submittedDate&amp;amp;sortOrder=descending
      &amp;amp;start=0&amp;amp;max_results=100
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No key. No login. Atom XML out. It is documented, sanctioned for programmatic use, and has been stable for well over a decade — the opposite of reverse-engineered endpoints that break monthly.&lt;/p&gt;

&lt;p&gt;The query grammar is small but composes well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fields:&lt;/strong&gt; &lt;code&gt;all:&lt;/code&gt; (title + abstract + more), &lt;code&gt;ti:&lt;/code&gt;, &lt;code&gt;abs:&lt;/code&gt;, &lt;code&gt;au:&lt;/code&gt;, &lt;code&gt;cat:&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boolean:&lt;/strong&gt; &lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;ANDNOT&lt;/code&gt;, with parentheses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrases:&lt;/strong&gt; double quotes, so &lt;code&gt;all:"retrieval augmented generation"&lt;/code&gt; matches the phrase, not the words scattered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "anything about tool use or agents, in the AI or NLP categories, by this lab" is one query string.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things worth knowing before you build
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Politeness is the rate limit.&lt;/strong&gt; The API guidance asks for about one request every 3 seconds. With &lt;code&gt;max_results=100&lt;/code&gt; per page, that is 2,000 papers a minute, which is more than any monitoring workflow needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort by &lt;code&gt;submittedDate&lt;/code&gt; descending and dedupe by ID.&lt;/strong&gt; arXiv IDs are versioned (&lt;code&gt;2507.01234v2&lt;/code&gt;), so decide whether a revision counts as "new" for you. For monitoring, tracking the ID without the version suffix and diffing daily is usually what people want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstracts are full text in the feed.&lt;/strong&gt; You do not need to touch a PDF to build a useful pipeline: the abstract is enough for embeddings, topic classification, and digest summaries. That turns "paper monitoring" into a pure JSON problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The workflow that actually keeps you current
&lt;/h2&gt;

&lt;p&gt;Nobody reads listing pages. The pattern that works is a scheduled diff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Daily run: query your topics, newest first.&lt;/li&gt;
&lt;li&gt;Skip every ID you have seen before.&lt;/li&gt;
&lt;li&gt;Push what is left to Slack, a newsletter draft, or an embedding index.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a feed of only-new, only-relevant papers with abstracts, which is what all the "stay current with AI research" tools sell, built on an API that gives the data away.&lt;/p&gt;

&lt;p&gt;I packaged this as an Apify actor this week: &lt;a href="https://apify.com/scrapemint/arxiv-papers-scraper" rel="noopener noreferrer"&gt;arXiv Papers Scraper&lt;/a&gt; takes keywords, categories, and authors, handles the pagination and politeness delays, normalizes rows (title, full abstract, authors, categories, dates, PDF link, DOI), and has cross-run dedupe built in for exactly the scheduled-diff workflow above. The first 2 rows of every run are free.&lt;/p&gt;

&lt;p&gt;One small irony to end on: the hardest part of building AI research tooling is not the AI. It is that most of the web fights being read by machines. arXiv does not, and it is not a coincidence that it is also the most machine-cited corpus in the field.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>machinelearning</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
