<?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: Noble Ronin</title>
    <description>The latest articles on DEV Community by Noble Ronin (@ronin13).</description>
    <link>https://dev.to/ronin13</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%2F4027486%2F6ad6877d-ae73-4dab-8cfd-d6f408944c56.png</url>
      <title>DEV Community: Noble Ronin</title>
      <link>https://dev.to/ronin13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ronin13"/>
    <language>en</language>
    <item>
      <title>I Wanted to Check If a Package Was Really Built by CI, Not Someone's Laptop. npm Shows It. PyPI Hides It. crates.io Never Says.</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 14 Sep 2026 09:54:27 +0000</pubDate>
      <link>https://dev.to/ronin13/i-wanted-to-check-if-a-package-was-really-built-by-ci-not-someones-laptop-npm-shows-it-pypi-2m3m</link>
      <guid>https://dev.to/ronin13/i-wanted-to-check-if-a-package-was-really-built-by-ci-not-someones-laptop-npm-shows-it-pypi-2m3m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2ycbaqfubq4s6qwk8gj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2ycbaqfubq4s6qwk8gj.png" alt="I Wanted to Check If a Package Was Really Built by CI, Not Someone's Laptop. npm Shows It. PyPI Hides It. crates.io Never Says." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I keep a small actor that normalizes package metadata across npm, PyPI and crates.io — version, license, downloads, that kind of thing. This week I wanted to add one more column: can I tell, from the registry's own public API, whether a given release was actually built by a CI job instead of pushed from someone's laptop with a stolen token?&lt;/p&gt;

&lt;p&gt;That's not an abstract question. Every incident I wrote up in an earlier piece — the &lt;code&gt;ua-parser-js&lt;/code&gt; hijack, the &lt;code&gt;node-ipc&lt;/code&gt; protestware, the &lt;code&gt;ctx&lt;/code&gt; credential theft — was exactly this: a legitimate maintainer's publish credentials, used from somewhere that wasn't the project's normal release pipeline. Build provenance is the mechanism specifically designed to catch that: it doesn't ask "is this code safe," it asks "did this tarball really come out of the workflow it claims to," and answers with something cryptographically checkable instead of a maintainer's word.&lt;/p&gt;

&lt;p&gt;So: three registries, three flavors of "does this feature exist and can I see it." The honest answer turned out to be "sometimes, and not where I expected."&lt;/p&gt;

&lt;h2&gt;
  
  
  npm just tells you. Well — half the time.
&lt;/h2&gt;

&lt;p&gt;npm's &lt;code&gt;--provenance&lt;/code&gt; flag, when a maintainer uses it, gets the registry to store a Sigstore-backed attestation right alongside the package. You can see it on the exact same endpoint you'd already be hitting for version or license data:&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="nt"&gt;-s&lt;/span&gt; https://registry.npmjs.org/zod | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import json, sys
d = json.load(sys.stdin)
latest = d['dist-tags']['latest']
dist = d['versions'][latest]['dist']
print('has attestations:', 'attestations' in dist)
print(dist.get('attestations'))
"&lt;/span&gt;
&lt;span class="c"&gt;# has attestations: True&lt;/span&gt;
&lt;span class="c"&gt;# {'url': 'https://registry.npmjs.org/-/npm/v1/attestations/zod@4.6.5',&lt;/span&gt;
&lt;span class="c"&gt;#  'provenance': {'predicateType': 'https://slsa.dev/provenance/v1'}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow that URL and you get a real Sigstore DSSE bundle with a Rekor transparency-log entry — a public, independently-checkable record that this exact tarball came from this exact build.&lt;/p&gt;

&lt;p&gt;I ran that check against 25 well-known packages. 13 of them (52%) have it: &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;vite&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, &lt;code&gt;turbo&lt;/code&gt;, &lt;code&gt;zod&lt;/code&gt;, &lt;code&gt;@actions/core&lt;/code&gt;, &lt;code&gt;semver&lt;/code&gt;. 12 don't — and the miss list is the interesting part: &lt;code&gt;eslint&lt;/code&gt;, &lt;code&gt;prettier&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt;, and, no joke, &lt;code&gt;npm&lt;/code&gt; itself. The tool that shipped the feature doesn't use it on its own package.&lt;/p&gt;

&lt;p&gt;I don't think that's laziness so much as inertia — provenance requires publishing from a specific, GitHub-Actions-shaped CI setup with &lt;code&gt;id-token: write&lt;/code&gt; permissions wired up, and a project that's been publishing the same way since before the feature existed has no forcing function to change it. The packages that have it skew toward projects young enough, or churny enough, that someone touched the release workflow recently.&lt;/p&gt;

&lt;h2&gt;
  
  
  PyPI has the same data. It's just not on the endpoint anyone actually queries.
&lt;/h2&gt;

&lt;p&gt;I assumed PyPI would lag here — Trusted Publishing (PyPI's OIDC equivalent) is younger than npm's provenance flag. So I checked the obvious place first:&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="nt"&gt;-s&lt;/span&gt; https://pypi.org/pypi/pip/json | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import json, sys
d = json.load(sys.stdin)
print(sorted(d['urls'][0].keys()))
"&lt;/span&gt;
&lt;span class="c"&gt;# ['comment_text', 'core-metadata', 'digests', 'downloads', 'filename',&lt;/span&gt;
&lt;span class="c"&gt;#  'has_sig', 'md5_digest', 'packagetype', 'python_version',&lt;/span&gt;
&lt;span class="c"&gt;#  'requires_python', 'size', 'upload_time', 'upload_time_iso_8601', 'url',&lt;/span&gt;
&lt;span class="c"&gt;#  'yanked', 'yanked_reason']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing. I checked this on 11 popular packages, key by key. Not one has anything provenance-shaped on the classic JSON API — the endpoint basically every scraper, including mine, uses.&lt;/p&gt;

&lt;p&gt;It turns out the data exists on a completely different endpoint: the PEP 691 "simple" API, and only if you ask for it by content type.&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="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.pypi.simple.v1+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://pypi.org/simple/pip/ | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import json, sys
files = json.load(sys.stdin)['files']
print(files[-1]['filename'], '→', files[-1].get('provenance'))
"&lt;/span&gt;
&lt;span class="c"&gt;# pip-26.2.1.tar.gz → https://pypi.org/integrity/pip/26.2.1/pip-26.2.1.tar.gz/provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That URL resolves to a real Sigstore certificate. I decoded the SAN field by hand instead of trusting a summary field, and it names &lt;code&gt;github.com/pypa/pip/.github/workflows/release.yml&lt;/code&gt;, the exact git ref, and the commit SHA the build ran from.&lt;/p&gt;

&lt;p&gt;Across 20 packages, 17 (85%) had this — noticeably &lt;em&gt;higher&lt;/em&gt; adoption than npm's 52% in my sample. Which makes the hiding spot the actual story: the registry that's ahead on adoption is the one whose own default API pretends the feature doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  crates.io doesn't say anything, even though it now asks.
&lt;/h2&gt;

&lt;p&gt;crates.io shipped its own OIDC "Trusted Publishing" in 2025 and expanded it into 2026, per what I could find outside this session (couldn't reach crates.io's docs live — more on that below). So I expected some trace of it in the one crates.io endpoint this sandbox can actually reach: the sparse index, the flat-file format &lt;code&gt;cargo&lt;/code&gt; itself reads on every dependency resolution.&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="nt"&gt;-s&lt;/span&gt; https://index.crates.io/se/rd/serde | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;span class="c"&gt;# keys: name, vers, deps, cksum, features, features2, yanked, rust_version,&lt;/span&gt;
&lt;span class="c"&gt;#       pubtime, v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result on &lt;code&gt;tokio&lt;/code&gt;. Ten fields, none of them about who built the thing or how. Trusted Publishing verifies identity at the moment of &lt;code&gt;cargo publish&lt;/code&gt; — but that verification doesn't leave a mark anywhere a downstream tool can later read. It's not that crates.io lags npm and PyPI on the feature; it's that even a crate published under full OIDC verification looks, from the sparse index, identical to one uploaded from a token on someone's laptop in 2015.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faqhoivx3gwe5tjaqugir.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faqhoivx3gwe5tjaqugir.png" alt="npm, PyPI and crates.io build provenance, side by side" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd push back on myself
&lt;/h2&gt;

&lt;p&gt;My 52%-for-npm number is almost certainly high versus the ecosystem average — I picked well-known, actively-CI'd projects (react, vite, turbo) rather than a random sample. A wider study I found via search (not something I could re-run live — jsDelivr's own stats endpoints aren't reachable this session) put real-world npm provenance adoption around 12.6% of eligible packages. My sample and that number aren't contradicting each other; they're measuring different populations, and I should have said so before publishing rather than after.&lt;/p&gt;

&lt;p&gt;I also didn't verify what "provenance present" actually buys you. An attestation proves &lt;em&gt;this tarball came from this workflow at this commit&lt;/em&gt; — it says nothing about whether that workflow or that commit was trustworthy. A compromised CI pipeline can publish a perfectly valid, fully attested, malicious package. I'm reporting whether the plumbing exists and where to find it, not making a safety claim.&lt;/p&gt;

&lt;p&gt;And the crates.io leg is the most exposed: I could only reach the sparse index this session, not crates.io's own docs or its web API, so "Trusted Publishing leaves no trace in the sparse index" is confirmed by reading the index directly — but I'm relying on a search result, not a live fetch, for the claim that Trusted Publishing exists there at all.&lt;/p&gt;

&lt;p&gt;One more limit worth naming: for all three registries, I checked whether the attestation/provenance &lt;em&gt;link&lt;/em&gt; was present, not whether the cryptographic material behind it actually verifies. I read the Sigstore bundle contents and eyeballed that the fields make sense (real Rekor log index, a plausible in-toto statement, a certificate SAN that resolves to the right repo) — I didn't run an actual &lt;code&gt;cosign verify-blob&lt;/code&gt; or equivalent against a downloaded tarball. "Present and well-formed" is a different, weaker claim than "verified," and a scanner built on this data would need to do the latter.&lt;/p&gt;

&lt;p&gt;Turning "does this row have real build provenance" into a normalized column across all three registries — instead of three different codepaths per project — is the next thing I want to add to &lt;a href="https://apify.com/ponderable_hydrometer/package-registry-scraper" rel="noopener noreferrer"&gt;Package Registry Scraper&lt;/a&gt;. The field-by-field breakdown of where each registry actually puts this data (and where it doesn't) is in the cheatsheet: &lt;a href="https://github.com/noble-ronin/package-provenance-data" rel="noopener noreferrer"&gt;noble-ronin/package-provenance-data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: if you already publish with &lt;code&gt;--provenance&lt;/code&gt; or Trusted Publishing, does anything downstream of you actually check it — a CI gate, a security tool, a human — or did you turn it on and nobody's looked since?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>I Wanted to Check If a Package Had Ever Shipped Malware. npm, PyPI and crates.io All Erased the Evidence.</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:23:57 +0000</pubDate>
      <link>https://dev.to/ronin13/i-wanted-to-check-if-a-package-had-ever-shipped-malware-npm-pypi-and-cratesio-all-erased-the-43h</link>
      <guid>https://dev.to/ronin13/i-wanted-to-check-if-a-package-had-ever-shipped-malware-npm-pypi-and-cratesio-all-erased-the-43h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1g6xi6hwpxfojm5ne4g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1g6xi6hwpxfojm5ne4g.png" alt="I Wanted to Check If a Package Had Ever Shipped Malware. npm, PyPI and crates.io All Erased the Evidence." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wanted to build something small: a curl-only check that answers "has this exact package version ever shipped something malicious," using nothing but the three registries' own public JSON APIs — no third-party advisory database, just the same endpoints &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;pip install&lt;/code&gt; and &lt;code&gt;cargo add&lt;/code&gt; already call.&lt;/p&gt;

&lt;p&gt;It doesn't work. Not because the data is hard to parse — because the data isn't there. All three registries, in three different ways, quietly erase the incidents instead of flagging them.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm keeps a "deprecated" field. It just doesn't use it here.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ua-parser-js&lt;/code&gt; got its maintainer account hijacked on October 22, 2021. For about four hours, versions &lt;code&gt;0.7.29&lt;/code&gt;, &lt;code&gt;0.8.0&lt;/code&gt; and &lt;code&gt;1.0.0&lt;/code&gt; shipped a cryptominer and a credential stealer to anyone who installed them.&lt;/p&gt;

&lt;p&gt;npm's registry API does have a mechanism for exactly this — the &lt;code&gt;deprecated&lt;/code&gt; field, visible per-version in &lt;code&gt;GET https://registry.npmjs.org/{name}&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://registry.npmjs.org/ua-parser-js | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import json,sys
vs = json.load(sys.stdin)['versions']
print(len(vs), 'total versions')
print(sum(1 for v in vs.values() if v.get('deprecated')), 'carry a deprecated field')
print('0.7.29' in vs, '0.8.0' in vs, '1.0.0' in vs)
"&lt;/span&gt;
&lt;span class="c"&gt;# 94 total versions&lt;/span&gt;
&lt;span class="c"&gt;# 65 carry a deprecated field&lt;/span&gt;
&lt;span class="c"&gt;# False False False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;65 of 94 versions are marked deprecated — mostly a generic "please upgrade" nag, plus one real CVE notice (a ReDoS bug, unrelated to the hijack). So the field isn't dead weight; npm does use it for genuine security nudges. It just never touches the three versions that actually did the damage, because those aren't deprecated. They're gone. Not in the response at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-ipc&lt;/code&gt; tells the same story with a twist. In March 2022 its maintainer shipped &lt;code&gt;10.1.1&lt;/code&gt; and &lt;code&gt;10.1.2&lt;/code&gt; with code that wiped files on machines it geolocated to Russia or Belarus — protest code aimed at the invasion of Ukraine, packaged as a dependency update. &lt;code&gt;10.1.3&lt;/code&gt; followed as the fix. Query the registry today and the version list jumps straight from &lt;code&gt;10.1.0&lt;/code&gt; to &lt;code&gt;11.0.0&lt;/code&gt;. All five 10.1.x releases — the two malicious ones and the fix — were pulled as a block. Only 1 of node-ipc's 76 remaining versions carries a &lt;code&gt;deprecated&lt;/code&gt; note, and it's about a Windows socket bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  PyPI doesn't bother with a version-level flag. It deletes the package.
&lt;/h2&gt;

&lt;p&gt;In May 2022, &lt;code&gt;ctx&lt;/code&gt; — a small, harmless-looking utility with a decade of history — got its abandoned domain re-registered by an attacker, who used that to take over the PyPI listing and push a version that exfiltrated AWS credentials to a Heroku endpoint.&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="nt"&gt;-s&lt;/span&gt; https://pypi.org/pypi/ctx/json
&lt;span class="c"&gt;# {"message": "Not Found"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire response. Not a version array with one flagged entry — a 404. If you're iterating a dependency list and checking each package's JSON endpoint for red flags, &lt;code&gt;ctx&lt;/code&gt; just looks like a package that never existed, which is a worse signal than "flagged," because it gives you nothing to alert on.&lt;/p&gt;

&lt;h2&gt;
  
  
  crates.io's yank mechanism is the most honest of the three. It still didn't record this one.
&lt;/h2&gt;

&lt;p&gt;This is the one that's still warm. On August 20, 2026, &lt;code&gt;arrayref&lt;/code&gt; — a small crate with roughly 245 million all-time downloads — got a malicious &lt;code&gt;0.3.10&lt;/code&gt; published through a compromised maintainer account. The attacker yanked the legitimate &lt;code&gt;0.3.5&lt;/code&gt;–&lt;code&gt;0.3.9&lt;/code&gt; first, which makes &lt;code&gt;cargo&lt;/code&gt; print a "yanked, consider updating" warning and nudges anyone building right then toward the only unyanked version left: the poisoned one. It was live for about 86 minutes before the crates.io team pulled it.&lt;/p&gt;

&lt;p&gt;I checked the sparse index — the actual endpoint &lt;code&gt;cargo&lt;/code&gt; itself reads, not the website — this morning:&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="nt"&gt;-s&lt;/span&gt; https://index.crates.io/ar/ra/arrayref | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import json,sys
for line in sys.stdin:
    d = json.loads(line)
    print(d['vers'], d['yanked'])
"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-6&lt;/span&gt;
&lt;span class="c"&gt;# 0.3.5 False&lt;/span&gt;
&lt;span class="c"&gt;# 0.3.6 False&lt;/span&gt;
&lt;span class="c"&gt;# 0.3.7 False&lt;/span&gt;
&lt;span class="c"&gt;# 0.3.8 False&lt;/span&gt;
&lt;span class="c"&gt;# 0.3.9 False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;15 version lines, &lt;code&gt;0.1.0&lt;/code&gt; through &lt;code&gt;0.3.9&lt;/code&gt;, every one &lt;code&gt;yanked: false&lt;/code&gt; again — the restore held. &lt;code&gt;0.3.10&lt;/code&gt;, the actual malware, isn't a 16th line with &lt;code&gt;yanked: true&lt;/code&gt;. It isn't in the response at all. crates.io has a field literally built for "this version is bad, don't use it, but here's the historical record" — and the one version that most needed that record instead got the npm/PyPI treatment: deleted, not flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd push back on myself
&lt;/h2&gt;

&lt;p&gt;Four incidents across three registries is a pattern I noticed, not a survey — I'm not claiming every yanked-or-unpublished version in these registries' history is a security incident (most unpublishes are mundane: a broken build, a maintainer mistake caught in the first hour). I also didn't check whether any &lt;em&gt;other&lt;/em&gt; field on these APIs carries incident metadata I'm missing — I read the whole JSON response for each of these four, but "I read the whole response for four packages" isn't "I audited the schema." And the honest caveat that matters most: OSV.dev, the GitHub Advisory Database and RustSec's own site are exactly the systems built to keep this record permanently, cross-referenced by CVE, and I didn't manage to reach any of them live this session (sandbox-blocked) to confirm they still list all four — I'm trusting general knowledge there, not a fresh check. So the actual claim is narrow: the registries' &lt;em&gt;own&lt;/em&gt; metadata endpoints, the ones a dependency-scanning script would hit first because they're free and require no separate API key, don't carry this signal. A serious scanner needs a second source anyway.&lt;/p&gt;

&lt;p&gt;I built these four checks by hand, one &lt;code&gt;curl&lt;/code&gt; at a time. Doing this systematically across a real &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;Cargo.lock&lt;/code&gt; — one row per dependency, current version plus whatever the registry will still tell you about its history — is the kind of normalization my &lt;a href="https://apify.com/ponderable_hydrometer/package-registry-scraper" rel="noopener noreferrer"&gt;Package Registry Scraper&lt;/a&gt; actor does. The field-by-field reference for what each registry's API returns (and doesn't) is in the cheatsheet: &lt;a href="https://github.com/noble-ronin/package-removed-versions" rel="noopener noreferrer"&gt;noble-ronin/package-removed-versions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: if your dependency-scanning setup checks a registry's own &lt;code&gt;deprecated&lt;/code&gt;/&lt;code&gt;yanked&lt;/code&gt; fields as one of its signals, does it also cross-check a separate advisory database — or would a compromised version that got fully unpublished, like three of these four, slip through as if it never shipped at all?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>npm Has One License Field. PyPI Has Three. crates.io Has Zero.</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:12:24 +0000</pubDate>
      <link>https://dev.to/ronin13/npm-has-one-license-field-pypi-has-three-cratesio-has-zero-2mpd</link>
      <guid>https://dev.to/ronin13/npm-has-one-license-field-pypi-has-three-cratesio-has-zero-2mpd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgllhcdkxlo4te4qh7nq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgllhcdkxlo4te4qh7nq1.png" alt="npm Has One License Field. PyPI Has Three. crates.io Has Zero." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I went looking for one honest answer to a boring question: "what license is this dependency under," using nothing but the package registry's own API — no GitHub, no cloning the repo, no opening a &lt;code&gt;LICENSE&lt;/code&gt; file. Just the JSON that &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;pip install&lt;/code&gt;, and &lt;code&gt;cargo add&lt;/code&gt; themselves already fetch.&lt;/p&gt;

&lt;p&gt;npm answered cleanly. The other two did not, and not in the way I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that just works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET https://registry.npmjs.org/{name}/latest&lt;/code&gt; returns a top-level &lt;code&gt;license&lt;/code&gt; field. I checked eight packages, some maintained, some famously not:&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="nt"&gt;-s&lt;/span&gt; https://registry.npmjs.org/left-pad/latest | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"import json,sys; d=json.load(sys.stdin); print(d['license'])"&lt;/span&gt;
&lt;span class="c"&gt;# WTFPL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React, Express, Lodash, Chalk, Faker, colors — MIT. &lt;code&gt;left-pad&lt;/code&gt; — WTFPL. &lt;code&gt;request&lt;/code&gt; — Apache-2.0. Eight for eight, one clean SPDX string each, no exceptions. &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;left-pad&lt;/code&gt; even carry a plain-English &lt;code&gt;deprecated&lt;/code&gt; note explaining why you shouldn't be using them anymore. I expected this to be the easy part of the article. It was the whole article, until I looked at the other two registries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one with three fields that don't agree
&lt;/h2&gt;

&lt;p&gt;PyPI's &lt;code&gt;GET https://pypi.org/pypi/{name}/json&lt;/code&gt; doesn't have one license field. It has three: &lt;code&gt;info.license&lt;/code&gt; (free text), &lt;code&gt;info.license_expression&lt;/code&gt; (a newer SPDX-format field), and &lt;code&gt;info.classifiers&lt;/code&gt;, some of which start with &lt;code&gt;License ::&lt;/code&gt;. I pulled eleven well-known packages and checked all three:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;package&lt;/th&gt;
&lt;th&gt;&lt;code&gt;license&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;license_expression&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;License ::&lt;/code&gt; classifiers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;requests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Apache-2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flask&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BSD-3-Clause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;numpy&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pandas&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(full license text pasted in)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;black&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;django&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BSD-3-Clause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ruff&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;polars&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(full copyright text pasted in)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hatchling&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uv&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MIT OR Apache-2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero of eleven have all three populated. Eight rely entirely on &lt;code&gt;license_expression&lt;/code&gt; — a field that didn't exist in this form until PEP 639 started rolling out. Three (&lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;pandas&lt;/code&gt;, &lt;code&gt;polars&lt;/code&gt;) have nothing there at all, and fall back to &lt;code&gt;license&lt;/code&gt; — which for two of them isn't a license &lt;em&gt;name&lt;/em&gt;, it's the entire license or copyright text pasted into a JSON string field, because that's what &lt;code&gt;license = {file = "LICENSE"}&lt;/code&gt; in old &lt;code&gt;setup.py&lt;/code&gt;/&lt;code&gt;pyproject.toml&lt;/code&gt; configs produces.&lt;/p&gt;

&lt;p&gt;If you'd written a script six months ago that just reads &lt;code&gt;info.license&lt;/code&gt; because that's the obviously-named field — the npm-shaped assumption — you'd get a clean answer for exactly one of these eleven packages and silence for the other ten, despite every single one of them absolutely having a real, well-known license. The field that actually has the answer for most of them (&lt;code&gt;license_expression&lt;/code&gt;) is the &lt;em&gt;new&lt;/em&gt; one, and it's only there because PyPI is mid-migration: new &lt;code&gt;License ::&lt;/code&gt; classifiers can't even be added anymore, but plenty of long-lived projects (pandas, polars) still carry the old pattern and haven't touched their packaging metadata to adopt the new field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one with nothing at all
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cargo&lt;/code&gt; doesn't talk to crates.io's website. It talks to a sparse index — a flat, fast, keyless endpoint at &lt;code&gt;index.crates.io&lt;/code&gt; that returns one JSON line per published version. I fetched five popular crates and read every field in the response:&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="nt"&gt;-s&lt;/span&gt; https://index.crates.io/se/rd/serde | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;vers&lt;/code&gt;, &lt;code&gt;deps&lt;/code&gt;, &lt;code&gt;cksum&lt;/code&gt;, &lt;code&gt;features&lt;/code&gt;, &lt;code&gt;yanked&lt;/code&gt; — for &lt;code&gt;serde&lt;/code&gt;, &lt;code&gt;tokio&lt;/code&gt;, &lt;code&gt;rand&lt;/code&gt;, &lt;code&gt;clap&lt;/code&gt;, &lt;code&gt;regex&lt;/code&gt;, every single one. No &lt;code&gt;license&lt;/code&gt; key. Not empty, not null — absent, on all five. The license does exist on crates.io, on a completely separate endpoint (&lt;code&gt;crates.io/api/v1/crates/{name}&lt;/code&gt;), which is the REST API the website itself uses — but that's not what &lt;code&gt;cargo&lt;/code&gt; fetches when it resolves your dependency tree, and (small, honest complication) I couldn't even reach that REST endpoint from this session to double check what it returns; it 403'd the same way most non-registry domains have for me the last several weeks. So this is a real gap in the index cargo actually speaks, confirmed by reading the raw response — not a claim that crates.io has no license data anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd push back on myself
&lt;/h2&gt;

&lt;p&gt;Eleven PyPI packages and five crates is a handful, not a survey — I picked well-known, actively-maintained-or-recently-relevant projects, which probably &lt;em&gt;overstates&lt;/em&gt; how often &lt;code&gt;license_expression&lt;/code&gt; shows up; a random sample of the long tail would likely skew further toward "nothing machine-readable at all." I also didn't verify that any of these self-reported license strings are &lt;em&gt;correct&lt;/em&gt; — a &lt;code&gt;license_expression&lt;/code&gt; of &lt;code&gt;MIT&lt;/code&gt; is exactly as trustworthy as whoever ran &lt;code&gt;pip build&lt;/code&gt; typed it in, on every registry, npm included. And I didn't check whether SPDX identifiers in the npm &lt;code&gt;license&lt;/code&gt; field are always valid SPDX (some older packages use &lt;code&gt;"license": "UNLICENSED"&lt;/code&gt; or arbitrary strings that aren't real identifiers) — I only checked that the field exists and is populated, not that it's semantically clean.&lt;/p&gt;

&lt;p&gt;I built this by hand-curling twenty-four URLs across three registries and eyeballing the JSON; running the same check across an actual &lt;code&gt;package.json&lt;/code&gt;/&lt;code&gt;requirements.txt&lt;/code&gt;/&lt;code&gt;Cargo.lock&lt;/code&gt; is what my &lt;a href="https://apify.com/ponderable_hydrometer/package-registry-scraper" rel="noopener noreferrer"&gt;Package Registry Scraper&lt;/a&gt; actor normalizes into one row per dependency. The full field reference — endpoints, exact JSON paths, and which registry currently has which of the three license fields — is in the cheatsheet: &lt;a href="https://github.com/noble-ronin/package-license-data" rel="noopener noreferrer"&gt;noble-ronin/package-license-data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: if you're writing an SBOM generator or a license-compliance scanner today, are you already checking all three PyPI fields before giving up on a package — or, like six-months-ago me, just the one that has "license" in its name?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>npm Will Tell You Who Maintains a Package. It Won't Tell You Why That Changed.</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 07:42:16 +0000</pubDate>
      <link>https://dev.to/ronin13/npm-will-tell-you-who-maintains-a-package-it-wont-tell-you-why-that-changed-1562</link>
      <guid>https://dev.to/ronin13/npm-will-tell-you-who-maintains-a-package-it-wont-tell-you-why-that-changed-1562</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2bmhingpv0ne8nfgjezv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2bmhingpv0ne8nfgjezv.png" alt="npm Will Tell You Who Maintains a Package" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last September, someone spent a bit of social-engineering effort on one npm account and, for about two hours, quietly rewrote code sitting underneath roughly 2.6 billion weekly downloads. The account belonged to a well-known maintainer who goes by Qix. The trick was an email that looked exactly like an npm 2FA-reset notice, sent from a domain built to look official, asking for a credential update before a deadline. It worked. The packages hit included &lt;code&gt;chalk&lt;/code&gt;, &lt;code&gt;debug&lt;/code&gt;, &lt;code&gt;ansi-styles&lt;/code&gt;, and about a dozen others — the kind of dependency you've never typed into &lt;code&gt;package.json&lt;/code&gt; yourself but that half your dependency tree pulls in anyway.&lt;/p&gt;

&lt;p&gt;A year(ish) later, I wanted to know something simple: does npm's own public API, right now, show anything different about those packages? Not "was there a CVE" — I mean the boring metadata. Who's listed as a maintainer today. It's a question anyone can ask, with no account and no rate-limit key, against an endpoint that's been sitting there this whole time.&lt;/p&gt;

&lt;p&gt;It does show something. It's just not the story I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually queried
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET https://registry.npmjs.org/{name}&lt;/code&gt; is keyless, no auth, and returns a &lt;code&gt;maintainers&lt;/code&gt; array — the npm accounts with push rights on that package, right now. I pulled it live today for the ten packages from the first wave of last September's attack, plus a few unrelated famous ones for contrast (&lt;code&gt;left-pad&lt;/code&gt;, &lt;code&gt;lodash&lt;/code&gt;, &lt;code&gt;is-odd&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://registry.npmjs.org/chalk | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"import json,sys; print([m['name'] for m in json.load(sys.stdin)['maintainers']])"&lt;/span&gt;
&lt;span class="c"&gt;# ["sindresorhus"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the maintainer list, as of today, for the packages that were actually hit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;package&lt;/th&gt;
&lt;th&gt;maintainer(s) today&lt;/th&gt;
&lt;th&gt;in Sept 2025 attack?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chalk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansi-styles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansi-regex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strip-ansi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wrap-ansi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;supports-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slice-ansi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sindresorhus&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;debug&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;qix, tootallnate&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;color-convert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;qix&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;color-name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dfcreative, moox, dy&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven of ten now list a single account — sindresorhus, one of the most prolific maintainers on the whole registry, and nobody involved in the incident — with zero trace of qix. Two (&lt;code&gt;debug&lt;/code&gt;, &lt;code&gt;color-convert&lt;/code&gt;) still list the same account that got phished. One (&lt;code&gt;color-name&lt;/code&gt;) lists three names that appear nowhere else in this table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I can't answer
&lt;/h2&gt;

&lt;p&gt;I went looking for the obvious follow-up: was this a deliberate remediation — GitHub/npm security stepping in and reassigning ownership after the incident — or something else? I didn't find a clean answer. The closest thing I found was a single source claiming Qix and sindresorhus were already co-maintainers on some of these packages &lt;em&gt;before&lt;/em&gt; the attack, which — if true — would mean what I'm looking at isn't a post-incident transfer at all, just qix's own access being removed at some point, for some reason, that I can't see from here.&lt;/p&gt;

&lt;p&gt;I want to be straight about this: I don't know which of those it is. And that's kind of the actual finding. The &lt;code&gt;maintainers&lt;/code&gt; field gives you a name, today. It doesn't give you a diff. It can't tell you "this account was added six days after a headline incident" versus "this account was already there and nothing changed." Whatever happened to &lt;code&gt;chalk&lt;/code&gt;'s ownership between last September and today, the only way I'd know is if someone had been diffing this exact endpoint daily for a year — which, as far as I can tell, nobody was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this gets worse, not better
&lt;/h2&gt;

&lt;p&gt;npm's &lt;code&gt;maintainers[]&lt;/code&gt; is actually the &lt;em&gt;good&lt;/em&gt; case. I checked the same question on the other two registries our actor covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;'s &lt;code&gt;info.maintainer&lt;/code&gt; / &lt;code&gt;info.maintainer_email&lt;/code&gt; fields are free text. &lt;code&gt;requests&lt;/code&gt; on PyPI lists &lt;code&gt;maintainer: null&lt;/code&gt; and a &lt;code&gt;maintainer_email&lt;/code&gt; string with two names in it — there's no enforced link between that string and an account that can actually push a release. Anyone with upload rights could put anything there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;crates.io&lt;/strong&gt;'s sparse index — the fast, keyless endpoint &lt;code&gt;cargo&lt;/code&gt; itself queries — doesn't carry an owner or maintainer field at all. You'd need the separate REST &lt;code&gt;owners&lt;/code&gt; endpoint for that, and (small, honest aside) I couldn't even reach &lt;code&gt;crates.io&lt;/code&gt;'s REST API from this session to check what &lt;em&gt;that&lt;/em&gt; one exposes — it 403'd, same as most non-registry domains this run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put differently: of the three registries our sample sits on, only npm makes "who can currently push a release" a first-class, queryable, enforced field. That's genuinely good — most ecosystems don't give you even that much. It's just not the whole picture, and it wasn't built to answer the question I was actually asking.&lt;/p&gt;

&lt;p&gt;So of the three registries, exactly one gives you a structured, enforced answer to "who can push code right now" — and that's the one that just demonstrated, live, that the answer can change in ways you have no way of noticing without watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd push back on myself
&lt;/h2&gt;

&lt;p&gt;Ten packages is not a sample, it's a highlight reel — I picked ones that were already famous for the wrong reasons. A single-maintainer package isn't inherently risky; most of them are perfectly fine forever, and &lt;code&gt;chalk&lt;/code&gt;/&lt;code&gt;debug&lt;/code&gt; having one or two maintainers was completely normal &lt;em&gt;before&lt;/em&gt; anyone had heard of Qix. This data also can't tell you whether 2FA is enforced, whether npm's own account-security posture has actually improved since September, or whether the accounts I'm now trusting have themselves been targeted since. I'm reading a snapshot and treating it like it means more than a snapshot can mean.&lt;/p&gt;

&lt;p&gt;The wider numbers I'm &lt;em&gt;not&lt;/em&gt; claiming to have measured — the ~2.6B weekly downloads across the September packages, axios's own separate single-maintainer compromise in March, an April audit finding 26 of the top 113 npm packages carrying similar structural risk — are all sourced from security write-ups, not something I pulled myself. I'm citing them, not reproducing them.&lt;/p&gt;

&lt;p&gt;I built this table by hand-curling eighteen URLs; checking the same fields across an actual dependency list is what my &lt;a href="https://apify.com/ponderable_hydrometer/package-registry-scraper" rel="noopener noreferrer"&gt;Package Registry Scraper&lt;/a&gt; actor does in one pass. The full endpoint reference — including the honest gaps in PyPI and crates.io's owner data — is in the cheatsheet: &lt;a href="https://github.com/noble-ronin/package-maintainer-data" rel="noopener noreferrer"&gt;noble-ronin/package-maintainer-data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, genuinely: if you maintain something popular enough to matter, would you &lt;em&gt;want&lt;/em&gt; npm to surface "this package's maintainer list changed on this date" as a public, queryable field — or does making "was this ever compromised, and when did ownership move" easy to query just hand attackers a target list of packages that are already known to have had one soft spot?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Tried to Name My Next Project. npm, PyPI and crates.io All Said No.</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:33:27 +0000</pubDate>
      <link>https://dev.to/ronin13/i-tried-to-name-my-next-project-npm-pypi-and-cratesio-all-said-no-5hn9</link>
      <guid>https://dev.to/ronin13/i-tried-to-name-my-next-project-npm-pypi-and-cratesio-all-said-no-5hn9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzxsqp98hnx6f3ny5fna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzxsqp98hnx6f3ny5fna.png" alt="I Tried to Name My Next Project. npm, PyPI and crates.io All Said No." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've got a new CLI tool half-written and no name for it. So before I got any further, I did the thing you're supposed to do: check if a good, short name is actually available before you build your identity around it.&lt;/p&gt;

&lt;p&gt;I picked ten words. Short, real English words, the kind that sound like a project name without trying too hard: &lt;code&gt;pulse&lt;/code&gt;, &lt;code&gt;grid&lt;/code&gt;, &lt;code&gt;flow&lt;/code&gt;, &lt;code&gt;spark&lt;/code&gt;, &lt;code&gt;nova&lt;/code&gt;, &lt;code&gt;orbit&lt;/code&gt;, &lt;code&gt;forge&lt;/code&gt;, &lt;code&gt;relay&lt;/code&gt;, &lt;code&gt;pilot&lt;/code&gt;, &lt;code&gt;atlas&lt;/code&gt;. Then I checked each one against npm, PyPI and crates.io — the three registries my tool would plausibly need a name on eventually, even though it only ships to one today.&lt;/p&gt;

&lt;p&gt;Ten words, three registries, thirty lookups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All thirty came back taken.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Not just taken — abandoned
&lt;/h2&gt;

&lt;p&gt;That was the annoying part but not the interesting part. The interesting part is &lt;em&gt;what&lt;/em&gt; was sitting on those names.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pilot&lt;/code&gt; on npm is one version, published in 2011, described as "a small library used in the creation of Ace editor." &lt;code&gt;atlas&lt;/code&gt; on npm has three published versions, and the description field is a literal unfilled template: &lt;code&gt;&amp;lt;p&amp;gt;&amp;amp;nbsp;&amp;lt;/p&amp;gt;&amp;lt;p align="center"&amp;gt;[Insert sweet logo here]&amp;lt;/p&amp;gt;&lt;/code&gt; — somebody scaffolded a project, published it to reserve the name, and never came back to write the README. &lt;code&gt;orbit&lt;/code&gt; and &lt;code&gt;forge&lt;/code&gt; on crates.io each have exactly one version, &lt;code&gt;0.0.1&lt;/code&gt; and &lt;code&gt;0.1.0&lt;/code&gt;. These aren't popular libraries I lost to. They're ten-plus-year-old ghosts, and the name is still gone.&lt;/p&gt;

&lt;p&gt;npm's own policy is explicit that this isn't supposed to happen — publishing a package purely to reserve a name for later is against their terms of use. In practice, nobody's coming to clean it up. A dead package from 2011 and a maintained one from last month occupy the namespace identically as far as &lt;code&gt;npm install&lt;/code&gt; is concerned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that actually worked
&lt;/h2&gt;

&lt;p&gt;Frustrated, I tried the obvious next move: two-word names. Same ten words, paired up — &lt;code&gt;novaforge&lt;/code&gt;, &lt;code&gt;orbitpilot&lt;/code&gt;, &lt;code&gt;relayflow&lt;/code&gt;, &lt;code&gt;sparkatlas&lt;/code&gt;, &lt;code&gt;gridpulse&lt;/code&gt;, &lt;code&gt;forgeorbit&lt;/code&gt;, &lt;code&gt;atlasnova&lt;/code&gt;, &lt;code&gt;pulsegrid&lt;/code&gt;, &lt;code&gt;pulseflow&lt;/code&gt;, &lt;code&gt;novarelay&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Thirty checks again. This time, &lt;strong&gt;26 of 30 came back free.&lt;/strong&gt; Eight of the ten compound names are completely open on npm, PyPI &lt;em&gt;and&lt;/em&gt; crates.io. Only two — &lt;code&gt;pulsegrid&lt;/code&gt; and &lt;code&gt;pulseflow&lt;/code&gt; — are taken, and only on npm and PyPI; both are still free on crates.io.&lt;/p&gt;

&lt;p&gt;Going from "single common word" to "two common words mashed together" took the collision rate from 100% to about 13%. That's a bigger effect than I expected from such a small change, and it's the actual, practical takeaway if you're naming something today: stop trying for the one perfect word. It's very likely gone, on every registry, and has been since 2011-2014.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd push back on this
&lt;/h2&gt;

&lt;p&gt;Ten words is an anecdote, not a survey. I picked words that &lt;em&gt;sound like project names&lt;/em&gt; — short, punchy, vaguely techy — which is exactly the category most likely to be squatted, so my 100% collision rate is probably an upper bound, not a typical rate for "any random word." A dictionary-sampled word (&lt;code&gt;teapot&lt;/code&gt;, &lt;code&gt;xylophone&lt;/code&gt;) would likely fare a lot better.&lt;/p&gt;

&lt;p&gt;I also can't tell you &lt;em&gt;when&lt;/em&gt; most of these got squatted, only that it was a long time ago and nobody's released a second version since. That's suggestive of abandonment, not proof — a one-version package with zero updates in 14 years is still, technically, active in the sense that &lt;code&gt;npm install&lt;/code&gt; returns it.&lt;/p&gt;

&lt;p&gt;And this only checked existence, not health. Some of these old squats might still get downloaded occasionally by something that depends on them transitively, which package-availability tools generally won't tell you either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth checking before you build, not after
&lt;/h2&gt;

&lt;p&gt;This isn't just a branding annoyance anymore. There's a live thread this year about attackers registering package names that AI coding assistants tend to hallucinate — so a coding agent that "remembers" a plausible-sounding package that never existed can get pointed straight at something someone else just claimed on purpose. Checking whether a name is real, and who holds it, isn't just about your own branding anymore.&lt;/p&gt;

&lt;p&gt;I ended up running all thirty lookups by hand with curl, which is fine for one project name. For anything past a handful of candidates — or if you want it as one normalized shape instead of three different JSON schemas — that's exactly what &lt;a href="https://apify.com/ponderable_hydrometer/package-registry-scraper" rel="noopener noreferrer"&gt;Package Registry Scraper&lt;/a&gt; does: feed it a list of candidate names, get back version/license/repo/dates from npm, PyPI and crates.io in one consistent row per package, so a &lt;code&gt;null&lt;/code&gt; result actually stands out.&lt;/p&gt;

&lt;p&gt;Full endpoint reference — including the crates.io sparse-index trick, since crates.io's own search API isn't the fastest way to check a single exact name — is in the &lt;a href="https://github.com/noble-ronin/package-registry-apis" rel="noopener noreferrer"&gt;cheatsheet repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the oldest, most obviously-abandoned squatted package name you've ever run into — and did you fight for it, or just pick something else?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Measured 300 Top DEV Posts Looking for the Title Formula. There Isn't One.</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Tue, 04 Aug 2026 13:04:39 +0000</pubDate>
      <link>https://dev.to/ronin13/i-measured-300-top-dev-posts-looking-for-the-title-formula-there-isnt-one-16kf</link>
      <guid>https://dev.to/ronin13/i-measured-300-top-dev-posts-looking-for-the-title-formula-there-isnt-one-16kf</guid>
      <description>&lt;p&gt;There is a whole genre of advice about how to title a technical post. Lead with a number. Start with "I". Make it a question. Be contrarian.&lt;/p&gt;

&lt;p&gt;I believed most of it. Then I got annoyed enough to check.&lt;/p&gt;

&lt;p&gt;The DEV API is public and doesn't need a key, so I pulled the top 300 posts from the last 30 days, threw out the ones tagged for the Brazilian community — that community votes as a bloc and would have skewed everything — and compared the top 100 by reactions against the 194 below them.&lt;/p&gt;

&lt;p&gt;I expected the winners' titles to look obviously different. They don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The titles are basically the same
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern in the title&lt;/th&gt;
&lt;th&gt;Top 100&lt;/th&gt;
&lt;th&gt;The rest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First person (I / My / Me)&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is a question&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colon or em-dash split&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contrarian word (wrong, stop, actually, broken, secret, nobody)&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contains a number&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Says "you" or "your"&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mentions AI&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the first four rows. First person, questions, the colon-subtitle construction — the three moves most often recommended — are as common among the posts nobody read as among the posts everybody read.&lt;/p&gt;

&lt;p&gt;The contrarian row is my favourite, because it runs backwards. Posts with "wrong" or "stop" or "secret" in the title were &lt;strong&gt;more&lt;/strong&gt; common in the bottom group. That's the single most-repeated piece of title advice, and in this sample it is mild evidence against.&lt;/p&gt;

&lt;p&gt;Three patterns do lean the right way: numbers, "you", and AI. But 33-versus-25 is not a formula. It's a nudge, and it's the kind of gap that a different month could easily flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually separates them is the topic
&lt;/h2&gt;

&lt;p&gt;Here are the tags in the top 100, by frequency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discuss       52      community     12
ai            50      javascript    10
programming   29      devchallenge   9
webdev        25      showdev        7
career        24      opensource     7
productivity  18      security       6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole answer, and it's much less fun than a title formula.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;discuss&lt;/code&gt; shows up in more than half the top hundred. &lt;code&gt;ai&lt;/code&gt; in half. Add &lt;code&gt;career&lt;/code&gt; and &lt;code&gt;productivity&lt;/code&gt; and you've described the majority of everything that travels here.&lt;/p&gt;

&lt;p&gt;Now look at the other end of the same list. The specific technical tags — the ones you'd reach for writing about a database, an API, your cluster — are all down in the long tail, in low single digits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api 3    devops 2    database 1    sql 1    kubernetes 1
linux 1  homelab 1   selfhosted 1  observability 1  infrastructure 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to be careful here, because my first pass at this got it wrong: I looked at the top of the tag list, saw none of them, and was about to write that they're &lt;em&gt;absent&lt;/em&gt;. They're not. They're just rare. There is a homelab-and-Talos post in that top 100, and an OpenTelemetry one. Deep technical work does break through.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;discuss&lt;/code&gt; appears 52 times and &lt;code&gt;database&lt;/code&gt; appears once. That's not a slope, it's a cliff. Writing in a specific technical domain isn't impossible — it's a bet at roughly fifty-to-one odds against, and most people making that bet have no idea those are the odds.&lt;/p&gt;

&lt;h2&gt;
  
  
  And the format: it's a conversation, not a reference
&lt;/h2&gt;

&lt;p&gt;One number explains more than everything above: the median top-100 post gets &lt;strong&gt;one comment for every two reactions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sit with that ratio for a second. On a normal post, comments are a small fraction of reactions — most people who like something tap the heart and leave. In the top hundred, half as many people &lt;em&gt;wrote a reply&lt;/em&gt; as tapped a heart.&lt;/p&gt;

&lt;p&gt;Those aren't posts people bookmarked. They're posts people argued with.&lt;/p&gt;

&lt;p&gt;That's also the mechanism, not just a correlation. Engagement is what earns a post its second and third round of distribution. A comment is a much stronger signal than a reaction, and it drags the post back up the feed for another pass. So the format that generates replies compounds, and the format that doesn't, dies quietly after its first impression.&lt;/p&gt;

&lt;p&gt;Which is a brutal thing to learn if you write reference material. A genuinely excellent tutorial — correct endpoints, tested examples, nothing hand-waved — gives a reader nothing to say back. "Thanks, this worked" is not a debate. The better and more complete it is, the less there is to add. Its quality actively suppresses the signal it needs to survive.&lt;/p&gt;

&lt;p&gt;Median reading time in the top 100, for the record: &lt;strong&gt;6 minutes&lt;/strong&gt;, with 80% falling between 2 and 11. Short, but not as short as the "nobody reads past 3 minutes" advice claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd argue with my own analysis
&lt;/h2&gt;

&lt;p&gt;Three holes, and I'd rather point at them than have you find them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correlation, obviously.&lt;/strong&gt; A post probably doesn't travel &lt;em&gt;because&lt;/em&gt; someone typed &lt;code&gt;discuss&lt;/code&gt; in the tag box. The tag describes a kind of post that was already going to travel. Retagging your database tutorial as &lt;code&gt;discuss&lt;/code&gt; will not save it, and if you try it, please report back, because I'd love to be wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reactions aren't reach.&lt;/strong&gt; I can only see public reaction counts, not views. It's possible some low-reaction posts are read constantly by people who never tap anything — the classic fate of a good reference article that ranks in Google and gets zero on-platform engagement. If that's happening, my "top 100" is really "top 100 at generating on-platform noise," which is a different thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I threw out data on a judgment call.&lt;/strong&gt; Excluding the pt-BR posts was defensible — different market, different voting dynamics — but it was still me removing rows because they were inconvenient for the comparison I wanted to make. You should discount the numbers slightly for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I bothered
&lt;/h2&gt;

&lt;p&gt;Because my own recent posts have zero reactions between them, and my instinct was that I was writing them badly.&lt;/p&gt;

&lt;p&gt;The data says I wasn't, particularly. I was writing them &lt;em&gt;in the wrong room&lt;/em&gt; — carefully, competently, into a category with nobody standing in it. Which is a more annoying problem than bad writing, because bad writing you can fix by trying harder, and this you can only fix by writing about something else.&lt;/p&gt;

&lt;p&gt;I haven't decided yet whether I want to.&lt;/p&gt;




&lt;p&gt;Run it yourself, it takes a minute:&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="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://dev.to/api/articles?top=30&amp;amp;per_page=100&amp;amp;page=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | "\(.public_reactions_count)\t\(.comments_count)\t\(.tag_list|join(","))\t\(.title)"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So here's what I actually want to know, because I only have one month of one platform and a lot of asterisks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Has anyone here moved a genuinely technical, reference-style post — real depth, real code — and gotten it read? What carried it?&lt;/strong&gt; I'm asking whether the trade-off is real or whether I've just proven that I gave up early.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>writing</category>
      <category>career</category>
      <category>programming</category>
    </item>
    <item>
      <title>Clinical Trials Data Has a Free JSON API — Every Study on ClinicalTrials.gov, No Key Required</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:47:49 +0000</pubDate>
      <link>https://dev.to/ronin13/clinical-trials-data-has-a-free-json-api-every-study-on-clinicaltrialsgov-no-key-required-1pek</link>
      <guid>https://dev.to/ronin13/clinical-trials-data-has-a-free-json-api-every-study-on-clinicaltrialsgov-no-key-required-1pek</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjkckjufuysaawpwtsi3v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjkckjufuysaawpwtsi3v.png" alt="Clinical Trials Data Has a Free JSON API" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever needed to know which drugs are in trial for a given condition, who's sponsoring a study, or whether a trial is still recruiting, you don't need to scrape the ClinicalTrials.gov website. The US National Library of Medicine runs an official &lt;strong&gt;v2 API&lt;/strong&gt; over its registry of &lt;strong&gt;500,000+ studies&lt;/strong&gt; — free, public, and &lt;strong&gt;keyless&lt;/strong&gt;. No sign-up, no token, no rate-limit tier to unlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The base
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://clinicaltrials.gov/api/v2/studies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is a &lt;code&gt;GET&lt;/code&gt; against that one endpoint with different query parameters — no auth header, no API key.&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://clinicaltrials.gov/api/v2/studies?query.cond=diabetes&amp;amp;pageSize=5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Query parameters
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Param&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query.cond&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Condition / disease (Essie expression syntax).&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query.cond=lung+cancer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query.intr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Intervention / drug / treatment.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query.intr=semaglutide&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query.spons&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lead sponsor or collaborator name.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query.spons=Pfizer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query.term&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Free-text search across fields (same expression syntax).&lt;/td&gt;
&lt;td&gt;&lt;code&gt;query.term=obesity+AND+phase2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filter.overallStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated status filter.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;filter.overallStatus=RECRUITING&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pageSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Studies per page — default &lt;strong&gt;10&lt;/strong&gt;, max &lt;strong&gt;1000&lt;/strong&gt;. Always set it explicitly.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pageSize=100&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pageToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cursor for the next page, taken from &lt;code&gt;nextPageToken&lt;/code&gt; in the previous response — &lt;strong&gt;not&lt;/strong&gt; a numeric offset.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pageToken=abc123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;countTotal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;true&lt;/code&gt; on the first request to get a &lt;code&gt;totalCount&lt;/code&gt; in the response.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;countTotal=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fields&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated field list, to shrink the payload when you don't need everything.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fields=NCTId,BriefTitle,OverallStatus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Recruiting trials for a specific drug, with a total count&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://clinicaltrials.gov/api/v2/studies?query.intr=semaglutide&amp;amp;filter.overallStatus=RECRUITING&amp;amp;pageSize=20&amp;amp;countTotal=true"&lt;/span&gt;

&lt;span class="c"&gt;# Trials sponsored by a specific company&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://clinicaltrials.gov/api/v2/studies?query.spons=Pfizer&amp;amp;pageSize=50"&lt;/span&gt;

&lt;span class="c"&gt;# Free-text search, narrower payload via `fields`&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://clinicaltrials.gov/api/v2/studies?query.term=obesity+AND+phase2&amp;amp;fields=NCTId,BriefTitle,OverallStatus,LeadSponsorName"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A response looks like this (field names abbreviated for readability — the real payload nests everything under &lt;code&gt;protocolSection&lt;/code&gt;):&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;"studies"&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;"protocolSection"&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;"identificationModule"&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;"nctId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NCT07091500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"briefTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLP-1 Receptor Agonist for Obesity"&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;"statusModule"&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;"overallStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RECRUITING"&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;"sponsorCollaboratorsModule"&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;"leadSponsor"&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;"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;"Example University"&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;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;"nextPageToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJ2IjoxfQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234&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;(The &lt;code&gt;1234&lt;/code&gt; above is a placeholder — run the &lt;code&gt;countTotal=true&lt;/code&gt; call yourself to get the live figure for your query; it changes as new trials are registered.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Page through everything
&lt;/h2&gt;

&lt;p&gt;Pagination is &lt;strong&gt;cursor-based&lt;/strong&gt;, not offset-based — grab &lt;code&gt;nextPageToken&lt;/code&gt; from each response and pass it back as &lt;code&gt;pageToken&lt;/code&gt; until the field is absent:&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://clinicaltrials.gov/api/v2/studies?query.cond=diabetes&amp;amp;pageSize=1000&amp;amp;pageToken=eyJ2IjoxfQ"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's in a study record
&lt;/h2&gt;

&lt;p&gt;Each study nests into modules — the ones people actually query for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt; — &lt;code&gt;nctId&lt;/code&gt;, &lt;code&gt;briefTitle&lt;/code&gt;, &lt;code&gt;officialTitle&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status &amp;amp; design&lt;/strong&gt; — &lt;code&gt;overallStatus&lt;/code&gt;, &lt;code&gt;phases&lt;/code&gt;, &lt;code&gt;studyType&lt;/code&gt;, &lt;code&gt;enrollmentInfo.count&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates&lt;/strong&gt; — &lt;code&gt;startDateStruct&lt;/code&gt;, &lt;code&gt;completionDateStruct&lt;/code&gt;, &lt;code&gt;lastUpdatePostDateStruct&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sponsors&lt;/strong&gt; — &lt;code&gt;leadSponsor.name&lt;/code&gt;, &lt;code&gt;collaborators[].name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clinical&lt;/strong&gt; — &lt;code&gt;conditions[]&lt;/code&gt;, &lt;code&gt;interventions[]&lt;/code&gt; (typed, e.g. &lt;code&gt;DRUG: Semaglutide&lt;/code&gt;), &lt;code&gt;briefSummary&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eligibility&lt;/strong&gt; — &lt;code&gt;sex&lt;/code&gt;, &lt;code&gt;minimumAge&lt;/code&gt;, &lt;code&gt;maximumAge&lt;/code&gt;, &lt;code&gt;healthyVolunteers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geography&lt;/strong&gt; — &lt;code&gt;contactsLocationsModule.locations[]&lt;/code&gt; (country, facility)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbyx6mswdmiw55xi47ld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbyx6mswdmiw55xi47ld.png" alt="Query the ClinicalTrials.gov API without an account" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The only two rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Be reasonable with volume — there's no published hard rate limit for anonymous use, but batch and cache rather than hammering the endpoint.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;pageToken&lt;/code&gt;, not a numeric offset, to page — the API doesn't support jumping to page N.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole thing. No account, no key, no scraping the SPA frontend (which just serves bootstrap JS to a screen-scraper anyway).&lt;/p&gt;




&lt;p&gt;If you'd rather skip the module-nesting, the pagination loop and flattening the response yourself, the &lt;a href="https://apify.com/ponderable_hydrometer/clinicaltrials-scraper" rel="noopener noreferrer"&gt;ClinicalTrials.gov Scraper&lt;/a&gt; on Apify wraps exactly these endpoints — condition/intervention/sponsor/status filters in, one flat row per study out.&lt;/p&gt;

&lt;p&gt;📌 Full endpoint cheatsheet (copy-paste reference): &lt;strong&gt;&lt;a href="https://github.com/noble-ronin/clinicaltrials-api" rel="noopener noreferrer"&gt;github.com/noble-ronin/clinicaltrials-api&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>healthcare</category>
      <category>data</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Earthquake Data Has a Free JSON API — Every Quake on Earth, No Key Required</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:35:21 +0000</pubDate>
      <link>https://dev.to/ronin13/earthquake-data-has-a-free-json-api-every-quake-on-earth-no-key-required-hch</link>
      <guid>https://dev.to/ronin13/earthquake-data-has-a-free-json-api-every-quake-on-earth-no-key-required-hch</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6047mwnbprb77dunyza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6047mwnbprb77dunyza.png" alt="Earthquake Data Has a Free JSON API" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever needed earthquake data — for a mapping app, a risk model, a news feed, or just a hobby dashboard — you don't need to scrape &lt;code&gt;earthquake.usgs.gov&lt;/code&gt;'s web pages, and you don't need a paid data vendor. The USGS runs the &lt;strong&gt;FDSN Event Web Service&lt;/strong&gt;, a free, &lt;strong&gt;keyless&lt;/strong&gt; JSON API over its entire global earthquake catalogue, updated continuously.&lt;/p&gt;

&lt;p&gt;No sign-up. No API key. No token. It's a public US government service and it's meant to be hit directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The base
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://earthquake.usgs.gov/fdsnws/event/1/query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask for GeoJSON and you get a standard &lt;code&gt;FeatureCollection&lt;/code&gt; — one &lt;code&gt;Feature&lt;/code&gt; per earthquake, each with a &lt;code&gt;geometry.coordinates&lt;/code&gt; (&lt;code&gt;[longitude, latitude, depthKm]&lt;/code&gt;) and a &lt;code&gt;properties&lt;/code&gt; object carrying everything else:&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://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&amp;amp;starttime=2026-07-01&amp;amp;minmagnitude=4.5&amp;amp;limit=5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each feature's &lt;code&gt;properties&lt;/code&gt; block includes &lt;code&gt;mag&lt;/code&gt;, &lt;code&gt;magType&lt;/code&gt;, &lt;code&gt;place&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt; / &lt;code&gt;updated&lt;/code&gt; (epoch ms), &lt;code&gt;tsunami&lt;/code&gt; (0/1 flag), &lt;code&gt;alert&lt;/code&gt; (PAGER level: &lt;code&gt;green&lt;/code&gt;/&lt;code&gt;yellow&lt;/code&gt;/&lt;code&gt;orange&lt;/code&gt;/&lt;code&gt;red&lt;/code&gt;), &lt;code&gt;sig&lt;/code&gt; (significance score), &lt;code&gt;felt&lt;/code&gt; (number of "did you feel it?" reports), &lt;code&gt;cdi&lt;/code&gt;/&lt;code&gt;mmi&lt;/code&gt; (intensity), &lt;code&gt;status&lt;/code&gt; (&lt;code&gt;automatic&lt;/code&gt;/&lt;code&gt;reviewed&lt;/code&gt;), &lt;code&gt;type&lt;/code&gt; (&lt;code&gt;earthquake&lt;/code&gt;, &lt;code&gt;quarry blast&lt;/code&gt;, …), plus a &lt;code&gt;detail&lt;/code&gt; URL for the full per-event record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Param&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;starttime&lt;/code&gt; / &lt;code&gt;endtime&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Time window, ISO or &lt;code&gt;YYYY-MM-DD&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;starttime=2026-07-01&amp;amp;endtime=2026-07-27&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;minmagnitude&lt;/code&gt; / &lt;code&gt;maxmagnitude&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Magnitude range&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minmagnitude=5.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;minlatitude&lt;/code&gt;/&lt;code&gt;maxlatitude&lt;/code&gt;/&lt;code&gt;minlongitude&lt;/code&gt;/&lt;code&gt;maxlongitude&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Bounding-box search&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minlatitude=32&amp;amp;maxlatitude=42&amp;amp;minlongitude=-125&amp;amp;maxlongitude=-114&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;latitude&lt;/code&gt;/&lt;code&gt;longitude&lt;/code&gt;/&lt;code&gt;maxradiuskm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Circle search around a point&lt;/td&gt;
&lt;td&gt;&lt;code&gt;latitude=35.4&amp;amp;longitude=139.7&amp;amp;maxradiuskm=200&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mindepth&lt;/code&gt; / &lt;code&gt;maxdepth&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Depth range, km&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maxdepth=70&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alertlevel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PAGER alert filter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alertlevel=orange&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;orderby&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;time&lt;/code&gt;, &lt;code&gt;time-asc&lt;/code&gt;, &lt;code&gt;magnitude&lt;/code&gt;, &lt;code&gt;magnitude-asc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;orderby=magnitude&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;geojson&lt;/code&gt; (recommended), &lt;code&gt;csv&lt;/code&gt;, &lt;code&gt;xml&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;format=geojson&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Every M4.5+ quake in the last window, newest first&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&amp;amp;minmagnitude=4.5&amp;amp;orderby=time"&lt;/span&gt;

&lt;span class="c"&gt;# California bounding box, this month&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&amp;amp;starttime=2026-07-01&amp;amp;minlatitude=32&amp;amp;maxlatitude=42&amp;amp;minlongitude=-125&amp;amp;maxlongitude=-114"&lt;/span&gt;

&lt;span class="c"&gt;# Within 200km of Tokyo, magnitude 5+&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&amp;amp;latitude=35.4&amp;amp;longitude=139.7&amp;amp;maxradiuskm=200&amp;amp;minmagnitude=5"&lt;/span&gt;

&lt;span class="c"&gt;# Only quakes that triggered a PAGER alert (orange/red = significant humanitarian impact)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&amp;amp;alertlevel=orange"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a rolling set of pre-built summary feeds if you just want "what's happening right now" without building a query — &lt;code&gt;https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/{significant,4.5,2.5,1.0,all}_{hour,day,week,month}.geojson&lt;/code&gt; — e.g. &lt;code&gt;.../summary/4.5_day.geojson&lt;/code&gt; for every M4.5+ quake in the last 24 hours. Same GeoJSON shape as the query endpoint, refreshed every few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes &amp;amp; gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyless, no rate-limit registration&lt;/strong&gt; — just be reasonable with request volume; it's a shared public service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;format=geojson&lt;/code&gt; is the easiest to work with; &lt;code&gt;format=text&lt;/code&gt; gives a pipe-delimited feed if you want something grep-friendly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt; starts as &lt;code&gt;automatic&lt;/code&gt; right after an event and flips to &lt;code&gt;reviewed&lt;/code&gt; once a seismologist confirms it — for anything safety-critical, filter on &lt;code&gt;status=reviewed&lt;/code&gt; or expect revisions.&lt;/li&gt;
&lt;li&gt;Depth and magnitude type (&lt;code&gt;magType&lt;/code&gt;) vary by network — &lt;code&gt;mww&lt;/code&gt;, &lt;code&gt;ml&lt;/code&gt;, &lt;code&gt;mb&lt;/code&gt; etc. aren't directly comparable across all ranges; don't average them naively.&lt;/li&gt;
&lt;li&gt;Circle search (&lt;code&gt;latitude&lt;/code&gt;/&lt;code&gt;longitude&lt;/code&gt;/&lt;code&gt;maxradiuskm&lt;/code&gt;) and bounding-box search are mutually exclusive — pick one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4vfbb7y935bkco0dgez9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4vfbb7y935bkco0dgez9.png" alt="Earthquake feed structure" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;p&gt;Full endpoint reference + copy-paste table: &lt;strong&gt;&lt;a href="https://github.com/noble-ronin/usgs-earthquake-api" rel="noopener noreferrer"&gt;noble-ronin/usgs-earthquake-api&lt;/a&gt;&lt;/strong&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;Want the filtered rows without building the query string yourself? The &lt;a href="https://apify.com/ponderable_hydrometer/earthquakes-scraper" rel="noopener noreferrer"&gt;Earthquake Data Scraper&lt;/a&gt; on Apify wraps this same endpoint — set magnitude/time/bbox in a form, get clean rows out, schedule it as a live alert feed.&lt;/p&gt;

</description>
      <category>api</category>
      <category>gis</category>
      <category>webscraping</category>
      <category>data</category>
    </item>
    <item>
      <title>I Pulled Live Jobs Straight From OpenAI, Stripe, Airbnb and Bosch — Each Took One API Call</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Sat, 25 Jul 2026 07:26:37 +0000</pubDate>
      <link>https://dev.to/ronin13/i-pulled-live-jobs-straight-from-openai-stripe-airbnb-and-bosch-each-took-one-api-call-3od5</link>
      <guid>https://dev.to/ronin13/i-pulled-live-jobs-straight-from-openai-stripe-airbnb-and-bosch-each-took-one-api-call-3od5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwl47x9tjdhtiy15mnbvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwl47x9tjdhtiy15mnbvr.png" alt="Live jobs from any company's ATS in one API call" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wanted live, structured job listings from a handful of big companies — no LinkedIn middleman, no Indeed anti-bot wall, no headless browser. So I just asked each company's own &lt;strong&gt;applicant tracking system&lt;/strong&gt; (ATS) directly.&lt;/p&gt;

&lt;p&gt;Here's what one keyless call returned, live today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; — &lt;strong&gt;755&lt;/strong&gt; open roles (Ashby)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bosch&lt;/strong&gt; — &lt;strong&gt;4,773&lt;/strong&gt; (SmartRecruiters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — &lt;strong&gt;535&lt;/strong&gt; (Greenhouse)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Palantir&lt;/strong&gt; — &lt;strong&gt;287&lt;/strong&gt; (Lever)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airbnb&lt;/strong&gt; — &lt;strong&gt;196&lt;/strong&gt; (Greenhouse)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spotify&lt;/strong&gt; — &lt;strong&gt;108&lt;/strong&gt; (Lever)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No sign-up. No API key. No token. No proxy. Every number above came from a plain &lt;code&gt;curl&lt;/code&gt; against the company's public board API — the same JSON their own careers page renders from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the ATS, not LinkedIn or Indeed
&lt;/h2&gt;

&lt;p&gt;LinkedIn and Indeed scrapers fight aggressive anti-bot systems. They break, they rate-limit, they silently return half the results, and you spend more time babysitting proxies than using the data.&lt;/p&gt;

&lt;p&gt;A company's ATS has no reason to fight you: it's the endpoint its &lt;strong&gt;own&lt;/strong&gt; careers page calls. It's the source of truth, it's fast, and for the big four ATS platforms below it's a clean public JSON API. You just have to know the shape of the call.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Greenhouse
&lt;/h3&gt;

&lt;p&gt;The board token is the slug in &lt;code&gt;boards.greenhouse.io/&amp;lt;token&amp;gt;&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;curl &lt;span class="s2"&gt;"https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns every posting with title, location, department and (with &lt;code&gt;content=true&lt;/code&gt;) the full HTML description. Stripe returned &lt;strong&gt;535&lt;/strong&gt; live roles; Airbnb, &lt;strong&gt;196&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lever
&lt;/h3&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://api.lever.co/v0/postings/palantir?mode=json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A flat JSON array — Palantir returned &lt;strong&gt;287&lt;/strong&gt; postings, each with categories, workplace type and apply URL. Spotify: &lt;strong&gt;108&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ashby
&lt;/h3&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://api.ashbyhq.com/posting-api/job-board/openai?includeCompensation=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenAI's entire board — &lt;strong&gt;755&lt;/strong&gt; roles — comes back in one response, compensation bands included where published.&lt;/p&gt;

&lt;h3&gt;
  
  
  SmartRecruiters
&lt;/h3&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://api.smartrecruiters.com/v1/companies/BoschGroup/postings?limit=100"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paginated with &lt;code&gt;limit&lt;/code&gt;/&lt;code&gt;offset&lt;/code&gt;. Bosch (&lt;code&gt;BoschGroup&lt;/code&gt;) exposes &lt;strong&gt;4,773&lt;/strong&gt; postings this way — a genuinely large, structured hiring dataset for free.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7v3bn95b6d1q9faaam1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7v3bn95b6d1q9faaam1w.png" alt="Nine ATS platforms, one detection-plus-fetch" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just the big four
&lt;/h2&gt;

&lt;p&gt;Those are the platforms with the marquee names, but the same "your ATS is already an API" trick works on more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workday&lt;/strong&gt; — &lt;code&gt;POST&lt;/code&gt; to the &lt;code&gt;/wday/cxs/.../jobs&lt;/code&gt; CXS endpoint (pass the full board URL)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BambooHR&lt;/strong&gt; — &lt;code&gt;{company}.bamboohr.com/careers/list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personio&lt;/strong&gt; — &lt;code&gt;{company}.jobs.personio.com/xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruitee&lt;/strong&gt; — &lt;code&gt;{company}.recruitee.com/api/offers/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breezy HR&lt;/strong&gt; — &lt;code&gt;{company}.breezy.hr/json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's &lt;strong&gt;nine&lt;/strong&gt; ATS platforms, each with a public listing endpoint. The exact URL patterns, response fields and gotchas for all nine are in this cheatsheet:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/noble-ronin/ats-job-apis" rel="noopener noreferrer"&gt;noble-ronin/ats-job-apis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest caveat
&lt;/h2&gt;

&lt;p&gt;Companies migrate. While checking this I hit &lt;code&gt;api.lever.co/v0/postings/brex&lt;/code&gt; and got back &lt;code&gt;{"ok":false,"error":"Document not found"}&lt;/code&gt; — Brex had moved off Lever. So in practice you want to &lt;strong&gt;detect&lt;/strong&gt; which ATS a company is on before you call it, and fall through the list until one answers.&lt;/p&gt;

&lt;p&gt;That detection-plus-fetch across all nine ATS is exactly what I wrapped into an actor, if you'd rather pass a company name and get clean rows out than wire up nine endpoints yourself:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://apify.com/ponderable_hydrometer/multi-ats-jobs" rel="noopener noreferrer"&gt;Multi-ATS Job Scraper&lt;/a&gt;&lt;/strong&gt; — give it a company or a careers-board URL, it auto-detects the ATS and returns structured, real-time jobs, with a monitor mode for new postings.&lt;/p&gt;

&lt;p&gt;But you don't need it to get started. Pick a company, guess their ATS from the careers-page URL, and run the matching &lt;code&gt;curl&lt;/code&gt; above. The odds it just works are better than you'd think.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All counts verified live on 2026-07-25 from an EU IP with no API key. Live numbers will drift as companies open and close roles.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webscraping</category>
      <category>jobs</category>
      <category>data</category>
    </item>
    <item>
      <title>FDA Recalls Have a Free JSON API — Every Drug, Device and Food Recall Without Scraping</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Fri, 24 Jul 2026 20:10:06 +0000</pubDate>
      <link>https://dev.to/ronin13/fda-recalls-have-a-free-json-api-every-drug-device-and-food-recall-without-scraping-4mn9</link>
      <guid>https://dev.to/ronin13/fda-recalls-have-a-free-json-api-every-drug-device-and-food-recall-without-scraping-4mn9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldk1gu2n6xox4hya06db.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldk1gu2n6xox4hya06db.png" alt="FDA Recalls Have a Free JSON API" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need FDA recall data — which drugs, devices and foods got pulled, why, by whom, and how serious it was — you do &lt;strong&gt;not&lt;/strong&gt; need to scrape fda.gov, and you do &lt;strong&gt;not&lt;/strong&gt; need a paid API. The FDA runs &lt;strong&gt;openFDA&lt;/strong&gt;, a free, &lt;strong&gt;keyless&lt;/strong&gt; JSON API over its own enforcement, adverse-event and labeling databases.&lt;/p&gt;

&lt;p&gt;No sign-up. No API key. No token. There's a higher rate limit if you register a free key, but every endpoint below works with nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The base
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.fda.gov
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recalls live under the &lt;strong&gt;enforcement&lt;/strong&gt; endpoints, one per product area:&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://api.fda.gov/drug/enforcement.json?limit=1"&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.fda.gov/device/enforcement.json?limit=1"&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.fda.gov/food/enforcement.json?limit=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each response has a &lt;code&gt;meta&lt;/code&gt; block (with &lt;code&gt;results.total&lt;/code&gt;) and a &lt;code&gt;results&lt;/code&gt; array. A single drug-recall record carries &lt;code&gt;recalling_firm&lt;/code&gt;, &lt;code&gt;product_description&lt;/code&gt;, &lt;code&gt;reason_for_recall&lt;/code&gt;, &lt;code&gt;classification&lt;/code&gt; (Class I / II / III — I is the most serious), &lt;code&gt;status&lt;/code&gt; (Ongoing / Completed / Terminated), &lt;code&gt;recall_initiation_date&lt;/code&gt;, &lt;code&gt;distribution_pattern&lt;/code&gt;, &lt;code&gt;code_info&lt;/code&gt;, plus an &lt;code&gt;openfda&lt;/code&gt; block that links the product back to NDC codes and brand/generic names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search with the &lt;code&gt;search&lt;/code&gt; parameter
&lt;/h2&gt;

&lt;p&gt;openFDA uses a simple &lt;code&gt;field:value&lt;/code&gt; query syntax. Quote multi-word values and URL-encode:&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;# All Class I (most serious) drug recalls&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://api.fda.gov/drug/enforcement.json?search=classification:"Class+I"&amp;amp;limit=5'&lt;/span&gt;

&lt;span class="c"&gt;# Recalls that are still Ongoing&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://api.fda.gov/drug/enforcement.json?search=status:"Ongoing"'&lt;/span&gt;

&lt;span class="c"&gt;# Ranges and booleans work too — food recalls initiated in 2026&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://api.fda.gov/food/enforcement.json?search=recall_initiation_date:[20260101+TO+20261231]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time of writing there are &lt;strong&gt;1,733&lt;/strong&gt; Class I drug recalls on record and &lt;strong&gt;2,580&lt;/strong&gt; with an &lt;code&gt;Ongoing&lt;/code&gt; status — all as structured JSON, no scraping.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd4etcydsp00xg2wtp5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd4etcydsp00xg2wtp5t.png" alt="Every drug, device and food recall as JSON" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Count instead of list
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;count=&amp;lt;field&amp;gt;.exact&lt;/code&gt; and openFDA returns an aggregation instead of raw rows — great for dashboards:&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://api.fda.gov/drug/enforcement.json?count=classification.exact"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right now that returns Class II = &lt;strong&gt;14,381&lt;/strong&gt;, Class I = &lt;strong&gt;1,733&lt;/strong&gt;, Class III = &lt;strong&gt;1,701&lt;/strong&gt;. One call, the whole distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page through everything
&lt;/h2&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://api.fda.gov/device/enforcement.json?limit=100&amp;amp;skip=200"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;limit&lt;/code&gt; (max 1000) and &lt;code&gt;skip&lt;/code&gt; page the results. The device enforcement set alone is &lt;strong&gt;39,539&lt;/strong&gt; records; food is &lt;strong&gt;29,238&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond recalls
&lt;/h2&gt;

&lt;p&gt;Same base, same keyless deal, for two more datasets people usually ask about in the same breath:&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;# Adverse event reports (FAERS)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.fda.gov/drug/event.json?limit=1"&lt;/span&gt;

&lt;span class="c"&gt;# Structured drug product labeling&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.fda.gov/drug/label.json?limit=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The only two rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Be reasonable with volume — anonymous calls are rate-limited (a registered free key just raises the ceiling).&lt;/li&gt;
&lt;li&gt;Page with &lt;code&gt;limit&lt;/code&gt; + &lt;code&gt;skip&lt;/code&gt;, and remember openFDA data is a &lt;strong&gt;snapshot&lt;/strong&gt; the FDA refreshes periodically, not a live wire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole thing.&lt;/p&gt;




&lt;p&gt;If you'd rather skip the paging, the &lt;code&gt;search&lt;/code&gt; encoding and the field-mapping, the &lt;a href="https://apify.com/ponderable_hydrometer/fda-recalls-scraper" rel="noopener noreferrer"&gt;FDA Recalls Scraper&lt;/a&gt; on Apify wraps exactly these endpoints — drug/device/food, class and status filters in, structured rows out.&lt;/p&gt;

&lt;p&gt;📌 Full endpoint cheatsheet (copy-paste reference): &lt;strong&gt;&lt;a href="https://github.com/noble-ronin/fda-recalls-api" rel="noopener noreferrer"&gt;github.com/noble-ronin/fda-recalls-api&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>healthcare</category>
      <category>webscraping</category>
      <category>data</category>
    </item>
    <item>
      <title>EU Public Tenders Have a Free JSON API — Every Notice, Buyer and Deadline Without Scraping</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:24:18 +0000</pubDate>
      <link>https://dev.to/ronin13/eu-public-tenders-have-a-free-json-api-every-notice-buyer-and-deadline-without-scraping-1c50</link>
      <guid>https://dev.to/ronin13/eu-public-tenders-have-a-free-json-api-every-notice-buyer-and-deadline-without-scraping-1c50</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzh32ujqc2geo0ah57k1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzh32ujqc2geo0ah57k1o.png" alt="EU Public Tenders Have a Free JSON API" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need EU public procurement data — who's buying what, from which government body, for how much, closing when — you do &lt;strong&gt;not&lt;/strong&gt; need a scraper, and you do &lt;strong&gt;not&lt;/strong&gt; need to pay one of the half-dozen "TED scraper" tools on the market. The EU's own &lt;strong&gt;TED&lt;/strong&gt; (Tenders Electronic Daily) journal exposes all of it as clean JSON through an official, keyless &lt;strong&gt;v3 search API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No sign-up. No API key. No token. Just a POST request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 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;POST https://api.ted.europa.eu/v3/notices/search
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anonymous access — search and retrieval require no authentication at all (only &lt;em&gt;submitting&lt;/em&gt; notices does).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull the newest notices
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://api.ted.europa.eu/v3/notices/search'&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;'Content-Type: application/json'&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;'{
    "query": "FT~\"cloud computing\" SORT BY publication-date DESC",
    "fields": ["publication-number", "notice-title", "buyer-name", "buyer-country", "deadline"],
    "limit": 10,
    "scope": "ACTIVE",
    "paginationMode": "ITERATION"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;query&lt;/code&gt; uses TED's expert-search syntax: &lt;code&gt;FT~"..."&lt;/code&gt; for full-text, field filters like &lt;code&gt;buyer-country=DEU&lt;/code&gt; or &lt;code&gt;PD&amp;gt;=20260101&lt;/code&gt; (publication date), combined with &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt;, and &lt;code&gt;SORT BY publication-date DESC&lt;/code&gt; to get live opportunities instead of the 2016 backlog. &lt;code&gt;fields&lt;/code&gt; is an explicit allowlist — ask for exactly the eForms fields you want back (&lt;code&gt;notice-title&lt;/code&gt; is always included even if you forget it).&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes back
&lt;/h2&gt;

&lt;p&gt;Each notice in the &lt;code&gt;notices&lt;/code&gt; array is keyed by the same eForms field names you requested — &lt;code&gt;notice-title&lt;/code&gt;, &lt;code&gt;buyer-name&lt;/code&gt;, &lt;code&gt;classification-cpv&lt;/code&gt; (procurement category codes), &lt;code&gt;total-value&lt;/code&gt; / &lt;code&gt;total-value-cur&lt;/code&gt;, &lt;code&gt;deadline&lt;/code&gt;, &lt;code&gt;links&lt;/code&gt;. Multilingual fields like the title arrive as an object keyed by language (&lt;code&gt;{"eng": [...], "fra": [...]}&lt;/code&gt;) rather than a plain string — grab &lt;code&gt;eng&lt;/code&gt; when present, fall back to whatever language is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter by country, value, category
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# German buyers, AI-related, published this year&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://api.ted.europa.eu/v3/notices/search'&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;'Content-Type: application/json'&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;'{"query": "FT~\"artificial intelligence\" AND buyer-country=DEU AND PD&amp;gt;=20260101 SORT BY publication-date DESC", "fields": ["publication-number","notice-title","buyer-name","total-value","deadline"], "limit": 50, "scope": "ACTIVE"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71ssy1oprwumyq08y3lj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71ssy1oprwumyq08y3lj.png" alt="Every EU tender notice as structured JSON" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Page through results
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;paginationMode: "ITERATION"&lt;/code&gt; and bump &lt;code&gt;page&lt;/code&gt; (or follow the pagination token TED returns) while &lt;code&gt;limit&lt;/code&gt; stays at or under 100 per call. &lt;code&gt;scope: "ACTIVE"&lt;/code&gt; returns currently-open notices; use &lt;code&gt;"ALL"&lt;/code&gt; to search the full historical archive (480,000+ notices back to the mid-2010s).&lt;/p&gt;

&lt;h2&gt;
  
  
  The one gotcha
&lt;/h2&gt;

&lt;p&gt;Field names are the &lt;em&gt;eForms&lt;/em&gt; names (&lt;code&gt;notice-title&lt;/code&gt;, &lt;code&gt;classification-cpv&lt;/code&gt;, &lt;code&gt;buyer-country&lt;/code&gt;) — not the old two-letter TED codes from the legacy API. If you copy a query from a pre-2023 blog post, expect it to 404 or return nothing; the schema changed with the eForms rollout.&lt;/p&gt;

&lt;p&gt;That's the whole thing: one POST endpoint, an expert-search query string, and a field allowlist — no key, no proxy, no anti-bot to fight.&lt;/p&gt;




&lt;p&gt;If you'd rather skip the pagination and the multilingual-field unwrapping, the &lt;a href="https://apify.com/ponderable_hydrometer/ted-tenders-scraper" rel="noopener noreferrer"&gt;EU Tenders Scraper&lt;/a&gt; on Apify wraps exactly this endpoint — keyword or expert query in, flat rows out (buyer, country, CPV, value, deadline, direct link).&lt;/p&gt;

&lt;p&gt;📌 Full field reference + copy-paste query cheatsheet: &lt;strong&gt;&lt;a href="https://github.com/noble-ronin/ted-tenders-api" rel="noopener noreferrer"&gt;github.com/noble-ronin/ted-tenders-api&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>government</category>
      <category>webscraping</category>
      <category>data</category>
    </item>
    <item>
      <title>US Regulations Have a Free JSON API — Every Rule, Notice and Executive Order Without Scraping</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Thu, 16 Jul 2026 11:47:52 +0000</pubDate>
      <link>https://dev.to/ronin13/us-regulations-have-a-free-json-api-every-rule-notice-and-executive-order-without-scraping-4o34</link>
      <guid>https://dev.to/ronin13/us-regulations-have-a-free-json-api-every-rule-notice-and-executive-order-without-scraping-4o34</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fts2ioy9enmvoxrwywr95.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fts2ioy9enmvoxrwywr95.png" alt="US Regulations Have a Free JSON API"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need US federal regulations as data — final rules, proposed rules, public notices, executive orders — you do &lt;strong&gt;not&lt;/strong&gt; need to scrape, and you do &lt;strong&gt;not&lt;/strong&gt; need a paid API. The &lt;strong&gt;Federal Register&lt;/strong&gt; (the daily journal of the US government) publishes &lt;em&gt;everything&lt;/em&gt; as clean JSON through a free, keyless API.&lt;/p&gt;

&lt;p&gt;No sign-up. No API key. No token. The only limit is a polite rate cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The base
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.federalregister.gov/api/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pull the latest documents
&lt;/h2&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://www.federalregister.gov/api/v1/documents.json?per_page=5&amp;amp;order=newest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;total_pages&lt;/code&gt;, and a &lt;code&gt;results&lt;/code&gt; array. Each result includes &lt;code&gt;document_number&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; (Rule / Proposed Rule / Notice / Presidential Document), &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;abstract&lt;/code&gt;, &lt;code&gt;publication_date&lt;/code&gt;, agencies, and direct &lt;code&gt;html_url&lt;/code&gt; and &lt;code&gt;pdf_url&lt;/code&gt; links to the official text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search by term, agency, date or type
&lt;/h2&gt;

&lt;p&gt;The API takes &lt;code&gt;conditions[...]&lt;/code&gt; filters:&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;# Everything mentioning "artificial intelligence"&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://www.federalregister.gov/api/v1/documents.json?conditions%5Bterm%5D=artificial+intelligence"&lt;/span&gt;

&lt;span class="c"&gt;# Final rules from the EPA, published this year&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://www.federalregister.gov/api/v1/documents.json?conditions%5Btype%5D%5B%5D=RULE&amp;amp;conditions%5Bagencies%5D%5B%5D=environmental-protection-agency&amp;amp;conditions%5Bpublication_date%5D%5Bgte%5D=2026-01-01"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time of writing, the plain &lt;code&gt;artificial intelligence&lt;/code&gt; term search returns &lt;strong&gt;1,504&lt;/strong&gt; documents — all as structured JSON.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbe0djht29bxsuybgxzeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbe0djht29bxsuybgxzeh.png" alt="Every rule, notice and executive order as JSON"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetch one document in full
&lt;/h2&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://www.federalregister.gov/api/v1/documents/2026-14454.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the full record for a single document number, including the abstract, the list of affected CFR parts, docket IDs, and links to the raw text (&lt;code&gt;body_html_url&lt;/code&gt;, &lt;code&gt;full_text_xml_url&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  List every agency
&lt;/h2&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://www.federalregister.gov/api/v1/agencies.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;472 agencies, each with its slug (use it in the &lt;code&gt;conditions[agencies][]&lt;/code&gt; filter above).&lt;/p&gt;

&lt;h2&gt;
  
  
  The only two rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Be reasonable with request volume — it's a public-good API, not a firehose to hammer.&lt;/li&gt;
&lt;li&gt;Page with &lt;code&gt;per_page&lt;/code&gt; (max 1000) and &lt;code&gt;page&lt;/code&gt;, or use &lt;code&gt;order=newest&lt;/code&gt; for a rolling feed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole thing. For consolidated &lt;strong&gt;regulations already in force&lt;/strong&gt; (as opposed to the daily journal of changes), pair this with the sibling &lt;strong&gt;eCFR&lt;/strong&gt; API — same idea, also keyless.&lt;/p&gt;




&lt;p&gt;If you'd rather skip the paging, the field-mapping and the retry logic, the &lt;a href="https://apify.com/ponderable_hydrometer/federal-register-scraper" rel="noopener noreferrer"&gt;Federal Register Scraper&lt;/a&gt; on Apify wraps exactly these endpoints — term/agency/type/date filters in, structured rows out.&lt;/p&gt;

&lt;p&gt;📌 Full endpoint cheatsheet (copy-paste reference): &lt;strong&gt;&lt;a href="https://github.com/noble-ronin/federal-register-api" rel="noopener noreferrer"&gt;github.com/noble-ronin/federal-register-api&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>government</category>
      <category>webscraping</category>
      <category>data</category>
    </item>
  </channel>
</rss>
