<?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: Project Ace</title>
    <description>The latest articles on DEV Community by Project Ace (@projectace01).</description>
    <link>https://dev.to/projectace01</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%2F4191439%2F1a4adccd-4448-4fc6-9715-aa5831f568bd.png</url>
      <title>DEV Community: Project Ace</title>
      <link>https://dev.to/projectace01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/projectace01"/>
    <language>en</language>
    <item>
      <title>10 years of financials for any US stock from the free SEC API (and the trap in the data)</title>
      <dc:creator>Project Ace</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:38:06 +0000</pubDate>
      <link>https://dev.to/projectace01/10-years-of-financials-for-any-us-stock-from-the-free-sec-api-and-the-trap-in-the-data-2k73</link>
      <guid>https://dev.to/projectace01/10-years-of-financials-for-any-us-stock-from-the-free-sec-api-and-the-trap-in-the-data-2k73</guid>
      <description>&lt;p&gt;The SEC gives you every US-listed company's reported financials for free, as JSON, no API key. Many people still scrape finance sites or pay for a data provider. Here is how to use the SEC data directly, and the one thing that will silently break your time series if you don't handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three endpoints you need
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ticker → CIK&lt;/strong&gt;: &lt;code&gt;https://www.sec.gov/files/company_tickers.json&lt;/code&gt; maps tickers like &lt;code&gt;AAPL&lt;/code&gt; to the SEC's company ID (CIK &lt;code&gt;320193&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All reported numbers&lt;/strong&gt;: &lt;code&gt;https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json&lt;/code&gt; (CIK padded to 10 digits). This has every XBRL fact the company ever filed: revenue, net income, assets, EPS, cash flow and hundreds more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filings list&lt;/strong&gt;: &lt;code&gt;https://data.sec.gov/submissions/CIK0000320193.json&lt;/code&gt; for 10-K, 10-Q, 8-K, Form 4 and so on, with links to the documents.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two rules from the SEC's fair-access policy: send a &lt;code&gt;User-Agent&lt;/code&gt; header with a contact email, and stay under &lt;strong&gt;10 requests per second&lt;/strong&gt;. Anonymous or bursty clients can get blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting annual numbers
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;companyfacts&lt;/code&gt;, each metric lives under &lt;code&gt;facts["us-gaap"][concept].units.USD&lt;/code&gt; as a list of facts. For annual values, keep facts where &lt;code&gt;form == "10-K"&lt;/code&gt; and &lt;code&gt;fp == "FY"&lt;/code&gt;, and for income-statement items also check that the period (&lt;code&gt;start&lt;/code&gt; to &lt;code&gt;end&lt;/code&gt;) is about a year long. Otherwise quarterly values can sneak in, because some companies also tag quarterly figures in their 10-K.&lt;/p&gt;

&lt;p&gt;Each 10-K repeats the previous two or three years, so the same year shows up several times. Group by the period's &lt;code&gt;end&lt;/code&gt; date and keep the most recently filed value (restatements win).&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: companies change concept names
&lt;/h2&gt;

&lt;p&gt;This is the part that bites. Apple's revenue over time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Used in 10-K filings for fiscal years&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SalesRevenueNet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2009–2017&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Revenues&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RevenueFromContractWithCustomerExcludingAssessedTax&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2019–2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Coca-Cola used &lt;code&gt;SalesRevenueGoodsNet&lt;/code&gt; until 2017 and &lt;code&gt;Revenues&lt;/code&gt; since 2018.&lt;/p&gt;

&lt;p&gt;The concept names changed when the revenue accounting standard ASC 606 came in. If you read only one concept, your chart either stops in 2017 or starts in 2019, and you won't get an error. You just get a shorter series.&lt;/p&gt;

&lt;p&gt;The fix is a priority list per metric. For each fiscal year, take the first concept that has a value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REVENUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RevenueFromContractWithCustomerExcludingAssessedTax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Revenues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SalesRevenueNet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RevenueFromContractWithCustomerIncludingAssessedTax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SalesRevenueGoodsNet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, Apple's revenue comes out cleanly from FY2016 ($215.6B) to FY2025 ($416.2B), and Coca-Cola's from 2014 to 2025.&lt;/p&gt;

&lt;p&gt;Some gaps are real, though: Coca-Cola does not report a &lt;code&gt;Liabilities&lt;/code&gt; total in its XBRL data, for example. Missing should stay missing rather than become zero.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was written with help from an AI assistant (Claude); the figures come from the SEC API, queried on 18 October 2026. Not investment advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>finance</category>
      <category>javascript</category>
      <category>data</category>
    </item>
    <item>
      <title>Monitoring Hacker News for your keywords? Most "hits" don't mention them</title>
      <dc:creator>Project Ace</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:21:45 +0000</pubDate>
      <link>https://dev.to/projectace01/monitoring-hacker-news-for-your-keywords-most-hits-dont-mention-them-5e74</link>
      <guid>https://dev.to/projectace01/monitoring-hacker-news-for-your-keywords-most-hits-dont-mention-them-5e74</guid>
      <description>&lt;p&gt;If you track mentions of your product, framework or competitor on Hacker News, you probably use the free Algolia HN Search API. It's great: fast, free, no key. But it has a catch that is easy to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;I searched HN &lt;strong&gt;comments since 1 October 2026&lt;/strong&gt; with &lt;code&gt;search_by_date&lt;/code&gt; and then checked each result for the keyword itself:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keyword&lt;/th&gt;
&lt;th&gt;Hits reported by Algolia&lt;/th&gt;
&lt;th&gt;Comments that actually contain the word&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rust&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;11,133&lt;/td&gt;
&lt;td&gt;1,715 (15 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deno&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,822&lt;/td&gt;
&lt;td&gt;443 (9 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;608&lt;/td&gt;
&lt;td&gt;582 (96 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Short words are the problem. Part of it is that Algolia also searches author names (&lt;code&gt;rust&lt;/code&gt; returns comments by a user called &lt;em&gt;rustyhancock&lt;/em&gt;), and it is typo-tolerant. But for &lt;code&gt;deno&lt;/code&gt;, almost none of the extra hits contain anything that looks like "deno" at all, and I could not pin down why. Distinctive words like &lt;code&gt;postgres&lt;/code&gt; are mostly fine.&lt;/p&gt;

&lt;p&gt;If you count mentions, build a dashboard or feed comments into an LLM for sentiment, 85–90 % noise ruins the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious fix doesn't work
&lt;/h2&gt;

&lt;p&gt;My first idea was to use Algolia's &lt;code&gt;_highlightResult&lt;/code&gt;: keep a hit if any field has &lt;code&gt;matchLevel != "none"&lt;/code&gt;. That looked perfect in a quick test, but Algolia &lt;strong&gt;only returns highlight data for roughly the first 70 hits&lt;/strong&gt; of a response. Everything after that has no highlights, so this filter silently throws away most of the &lt;em&gt;good&lt;/em&gt; results too (for &lt;code&gt;postgres&lt;/code&gt; it kept 74 of 717 real matches). I shipped that bug for about an hour before noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works
&lt;/h2&gt;

&lt;p&gt;Check the text yourself. Keep a hit only if every keyword appears at the start of a word in the title, comment text or URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`(^|[^a-z0-9])&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.*+?^${}()|[&lt;/span&gt;&lt;span class="se"&gt;\]\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isRealMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;story_title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment_text&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Start of a word" keeps &lt;code&gt;postgres&lt;/code&gt; → &lt;code&gt;PostgreSQL&lt;/code&gt; but drops &lt;code&gt;rust&lt;/code&gt; → &lt;code&gt;trust&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two more things you'll hit when collecting more than a few hundred items:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 1,000-hit cap.&lt;/strong&gt; Algolia returns at most 1,000 results per query. With &lt;code&gt;search_by_date&lt;/code&gt;, take the oldest &lt;code&gt;created_at_i&lt;/code&gt; of each page and query again with &lt;code&gt;numericFilters=created_at_i&amp;lt;{oldest}&lt;/code&gt; to walk back in time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering shrinks pages.&lt;/strong&gt; After filtering, a page of 1,000 hits might give you 80 items, so keep walking until you have enough.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was written with help from an AI assistant (Claude); the numbers come from my own queries on 18 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hackernews</category>
      <category>api</category>
      <category>javascript</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
