<?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: Tien Luong</title>
    <description>The latest articles on DEV Community by Tien Luong (@tien_luong_1ca9819f1fdb27).</description>
    <link>https://dev.to/tien_luong_1ca9819f1fdb27</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%2F4214510%2F8bcaa52a-e769-41c3-b0ba-760b1c325eaa.png</url>
      <title>DEV Community: Tien Luong</title>
      <link>https://dev.to/tien_luong_1ca9819f1fdb27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tien_luong_1ca9819f1fdb27"/>
    <language>en</language>
    <item>
      <title>I Crawled 554 SaaS Affiliate Pages. My Parser Lied to Me Nine Times.</title>
      <dc:creator>Tien Luong</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:14:38 +0000</pubDate>
      <link>https://dev.to/tien_luong_1ca9819f1fdb27/i-crawled-554-saas-affiliate-pages-my-parser-lied-to-me-nine-times-ddh</link>
      <guid>https://dev.to/tien_luong_1ca9819f1fdb27/i-crawled-554-saas-affiliate-pages-my-parser-lied-to-me-nine-times-ddh</guid>
      <description>&lt;p&gt;I wanted to know which SaaS companies actually pay affiliates well. Not "best affiliate programs 2026" listicles — those are themselves affiliate pages, written by people who get paid when you click. I wanted the terms straight off the vendors' own sites.&lt;/p&gt;

&lt;p&gt;So I wrote a crawler. 554 domains in, 151 programs out, 22 minutes.&lt;/p&gt;

&lt;p&gt;Here's the headline number: &lt;strong&gt;of 151 programs, only 17 pay lifetime commission&lt;/strong&gt; — and &lt;strong&gt;58 of them don't disclose the payout structure at all.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correction, 30 October 2026.&lt;/strong&gt; This post first reported &lt;strong&gt;19&lt;/strong&gt; lifetime programs. I have since hand-checked all 19 of those pages and found two misclassified: Omnisend states a 24-month cap in the same sentence as its lifetime clause, and Woodpecker's affiliate paths all return 200 while redirecting to its homepage — a soft 404 my crawler had no defence against. The correct figure is &lt;strong&gt;17&lt;/strong&gt;. The parser bug behind the Omnisend case is fixed and covered by tests. Every figure below is updated; the original numbers are shown where the change matters. Of those 17, only 9 say lifetime without a condition elsewhere on the page walking it back — that check is its own post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that number took four full rewrites to trust, because my parser was confidently wrong nine separate times. The bugs are the interesting part, so that's most of what this post is about.&lt;/p&gt;

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

&lt;p&gt;Node 26, standard library only. No cheerio, no puppeteer, no axios.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node:sqlite&lt;/code&gt; for storage (&lt;code&gt;DatabaseSync&lt;/code&gt; is stable now — no &lt;code&gt;better-sqlite3&lt;/code&gt; needed)&lt;/li&gt;
&lt;li&gt;global &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;AbortController&lt;/code&gt; for timeouts&lt;/li&gt;
&lt;li&gt;regex for HTML, which I'll defend in a second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow per domain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch &lt;code&gt;/robots.txt&lt;/code&gt;, parse the &lt;code&gt;User-agent: *&lt;/code&gt; block, honor every &lt;code&gt;Disallow&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fetch the homepage, scan &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags for anything matching &lt;code&gt;affiliate|partner program|referral&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Probe 14 conventional paths: &lt;code&gt;/affiliates&lt;/code&gt;, &lt;code&gt;/affiliate-program&lt;/code&gt;, &lt;code&gt;/partners/affiliate&lt;/code&gt;, &lt;code&gt;/become-an-affiliate&lt;/code&gt;, and so on.&lt;/li&gt;
&lt;li&gt;Score every candidate URL, fetch the best 8, extract terms.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Politeness: 5 concurrent domains, 700ms between hits on the same host, 20s timeout, honest User-Agent with the tool name in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On regex-for-HTML:&lt;/strong&gt; I'm not parsing a document tree. I'm looking for the sentence &lt;code&gt;"Earn 30% recurring commission"&lt;/code&gt; in a 460KB React-rendered blob where the DOM structure carries no meaning — the number and the word &lt;em&gt;recurring&lt;/em&gt; are nine &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;s apart for styling reasons. Strip tags, collapse whitespace, pattern-match the prose. A real parser would buy me nothing here.&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;function&lt;/span&gt; &lt;span class="nf"&gt;bocThe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&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;html&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;script&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&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;script&amp;gt;/gi&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="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;style&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&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;style&amp;gt;/gi&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="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;noscript&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&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;noscript&amp;gt;/gi&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="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;[\s\S]&lt;/span&gt;&lt;span class="sr"&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="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="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;amp;nbsp;/gi&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="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;amp;amp;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&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="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;amp;#36;/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="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;\s&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="s1"&gt; &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;span class="nf"&gt;trim&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;That's the whole HTML layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lie #1: It confused "sells affiliate software" with "pays commission"
&lt;/h2&gt;

&lt;p&gt;First run, Systeme.io came back at &lt;strong&gt;30%&lt;/strong&gt;. The real number is &lt;strong&gt;60%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The crawler had landed on &lt;code&gt;systeme.io/affiliate-management&lt;/code&gt; — Systeme's product page for businesses who want to &lt;em&gt;run their own&lt;/em&gt; affiliate program. The actual affiliate program lives at &lt;code&gt;/affiliate-program&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both pages match &lt;code&gt;/affiliate/&lt;/code&gt;. Both are full of the word &lt;em&gt;commission&lt;/em&gt;. One pays you; one sells you software for paying other people.&lt;/p&gt;

&lt;p&gt;Two fixes. First, score the URL shape:&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;URL_TOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;            &lt;span class="c1"&gt;// good&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;affiliate&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;program&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&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;become&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;an&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;affiliate&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&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;affiliates&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;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;],&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;partner&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;program&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&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;URL_XAU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;            &lt;span class="c1"&gt;// bad&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/affiliate&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;management|software|platform|tracking|tool&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;],&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;blog|resources|guides&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;|articles&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;|academy&lt;/span&gt;&lt;span class="se"&gt;)\/&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;],&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;features&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;|products&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;|solutions&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;|integrations&lt;/span&gt;&lt;span class="se"&gt;?)\/&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&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;pricing&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;compare&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;vs&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;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&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;Second — and this one caught cases the URL didn't — read which side of the transaction the page is addressing:&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;BEN_THAM_GIA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/join &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;our|the&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; affiliate|become an affiliate|you&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;'ll| will&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt; earn|refer&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]{0,30}&lt;/span&gt;&lt;span class="sr"&gt;and earn|start &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;promoting|earning&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/gi&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;BEN_BAN_PHAN_MEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/your affiliates&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|manage &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;your &lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;affiliates|affiliate &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;software|platform|management&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|launch your &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;own &lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;affiliate|recruit affiliates/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count both. If the sell-side wins, discard the page. &lt;code&gt;"you'll earn"&lt;/code&gt; and &lt;code&gt;"your affiliates"&lt;/code&gt; are the same topic pointed in opposite directions, and that single distinction fixed a whole class of false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #2: It counted competitor names as the network in use
&lt;/h2&gt;

&lt;p&gt;That same Systeme page name-drops PartnerStack and Tapfiliate — as &lt;strong&gt;competitors&lt;/strong&gt;, in a comparison table.&lt;/p&gt;

&lt;p&gt;My network detector was running regex over the full page text, so it tagged Systeme as running on both.&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="c1"&gt;// Before: /partnerstack/i.test(text)&lt;/span&gt;
&lt;span class="c1"&gt;// After:&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doMang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;lien&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&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;href|src|action&lt;/span&gt;&lt;span class="se"&gt;)\s&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="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]([^&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="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;/gi&lt;/span&gt;&lt;span class="p"&gt;)]&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;m&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;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;join&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;MANG&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="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;lien&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;ten&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;ten&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;A company that &lt;em&gt;runs&lt;/em&gt; on PartnerStack has a &lt;code&gt;partnerstack.com&lt;/code&gt; URL in an href, a tracking script, or a form action. A company that merely &lt;em&gt;mentions&lt;/em&gt; PartnerStack has it in a paragraph. Scan attribute values, not prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #3: It read the cookie duration backwards
&lt;/h2&gt;

&lt;p&gt;Systeme's terms table says, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lifetime, referral tagged to you permanently, not just for 30/60 days&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My parser recorded: &lt;strong&gt;cookie 30 days.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It found &lt;code&gt;30&lt;/code&gt;, it found &lt;code&gt;days&lt;/code&gt;, it was adjacent — correct pattern, exactly inverted meaning. The number is there precisely to be negated.&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;function&lt;/span&gt; &lt;span class="nf"&gt;trichCookie&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;span class="c1"&gt;// Check lifetime FIRST. Pages write "permanent, not just for 30/60 days" —&lt;/span&gt;
  &lt;span class="c1"&gt;// grabbing the number inverts the meaning.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/lifetime&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]{0,30}(?:&lt;/span&gt;&lt;span class="sr"&gt;cookie|attribution&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|never expires&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;|tagged to you permanently/i&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;9999&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\d{1,3})[\s&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*day&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;s&lt;/span&gt;&lt;span class="se"&gt;]?[\s&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;tracking&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;cookie/i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;quanh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/not just|rather than|unlike|instead of|more than/i&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;quanh&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;10&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;Two rules, and they generalize: &lt;strong&gt;check the strong claim before the weak one&lt;/strong&gt;, and &lt;strong&gt;look at what surrounds a number before believing it.&lt;/strong&gt; A regex match tells you a pattern occurred. It tells you nothing about whether the sentence was affirming or denying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #4: It grabbed prices, mockups, and totals as per-sale payouts
&lt;/h2&gt;

&lt;p&gt;This is where the output got funny.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Parser said&lt;/th&gt;
&lt;th&gt;What it actually read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writesonic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$23,940 per sale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A number in a marketing screenshot of an earnings dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;make.com&lt;/td&gt;
&lt;td&gt;$1,433.04&lt;/td&gt;
&lt;td&gt;A one-year total for 10 referrals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hunter.io&lt;/td&gt;
&lt;td&gt;$297.30&lt;/td&gt;
&lt;td&gt;Total for 10 referrals — real rate is $27.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;systeme.io&lt;/td&gt;
&lt;td&gt;$970&lt;/td&gt;
&lt;td&gt;The annual &lt;strong&gt;plan price&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four different failure modes, one root cause: a &lt;code&gt;$&lt;/code&gt; followed by digits is not a commission. It's a dollar amount on a page that contains many dollar amounts, most of which are prices.&lt;/p&gt;

&lt;p&gt;The fix was to require the payout unit to be adjacent:&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;DON_VI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(sale|referral|customer|signup|conversion|subscriber|client|order)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mau&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s?([&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;d,]+(?:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;d{1,2})?)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s*(?:USD&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s*)?(?:per|&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;/|for each|for every)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s*(?:new&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+|qualified&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+|paying&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+)?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DON_VI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(?:earn|get|receive|pay(?:s|ing)?|up to)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s?([&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;d,]+(?:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;d{1,2})?)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s*(?:USD&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s*)?(?:per|for each|for every)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+(?:new&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+|qualified&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+|paying&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;s+)?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DON_VI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gi&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;span class="c1"&gt;// ...and a bound the first version simply did not have:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tien&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;tien&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus explicit rejections for &lt;code&gt;/year&lt;/code&gt;, &lt;code&gt;/month&lt;/code&gt;, &lt;code&gt;refer \d+&lt;/code&gt;, &lt;code&gt;from \d+ users&lt;/code&gt;, &lt;code&gt;this year&lt;/code&gt; — the grammar of an aggregate rather than a unit rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$23,940 should have been impossible.&lt;/strong&gt; I had no sanity bound on the output at all. A parser that can emit a number four orders of magnitude outside the plausible range is a parser with no range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #5: The word "lifetime" elsewhere on the page overrode the real term
&lt;/h2&gt;

&lt;p&gt;Kartra's page, live right now:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Up to 40% recurring commissions for the first 12 months per qualified referral.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in its own FAQ, unambiguously:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do you pay lifetime commissions?&lt;/strong&gt; We do not offer lifetime commissions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My parser classified Kartra as &lt;strong&gt;lifetime&lt;/strong&gt;. That's a 30-point swing in the ranking — it would have sat near the top of the list on a term it explicitly denies offering.&lt;/p&gt;

&lt;p&gt;The word &lt;em&gt;lifetime&lt;/em&gt; appeared somewhere on the page. My classifier checked the lifetime pattern before the duration pattern, and returned on first match.&lt;/p&gt;

&lt;p&gt;The fix is ordering, and it's the opposite of lie #3's fix — which is the real lesson:&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="c1"&gt;// "first N months" beats "lifetime". A concrete duration is a harder&lt;/span&gt;
&lt;span class="c1"&gt;// commitment than a vague adjective, so it wins.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COTHANG&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kieu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;định kỳ có hạn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;thang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TRONDOI&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;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kieu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trọn đời&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;thang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;em&gt;cookies&lt;/em&gt;, lifetime wins because the number is usually being negated. For &lt;em&gt;commission terms&lt;/em&gt;, the number wins because the adjective is usually marketing. There's no universal precedence rule. You have to know what the page is doing with each field.&lt;/p&gt;

&lt;p&gt;Related: snov.io writes &lt;strong&gt;"Get 1 year lifetime commission"&lt;/strong&gt; — self-contradictory in four words. I take the concrete duration: 12 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #6: Earnings calculators poisoned everything
&lt;/h2&gt;

&lt;p&gt;lemlist's page says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Earn a 25% commissions for 12 months for each lemlist license purchased&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My parser recorded &lt;strong&gt;2 months&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The page has an interactive earnings calculator — a slider for referral count, a monthly price, projected totals. That widget's numbers are in the DOM. So are the pricing table's, the footer's, and the testimonials'.&lt;/p&gt;

&lt;p&gt;I was scanning the entire page for &lt;code&gt;/(\d+)\s*months?/&lt;/code&gt; and taking what I found.&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;function&lt;/span&gt; &lt;span class="nf"&gt;vungHoaHong&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;span class="c1"&gt;// Scan ONLY ±130 chars around commission keywords. A whole-page scan&lt;/span&gt;
  &lt;span class="c1"&gt;// picks up earnings calculators, pricing tables, footers.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doan&lt;/span&gt; &lt;span class="o"&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;re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/commission|recurring|you earn|payout|rev&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*share/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&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;exec&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;doan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;doan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&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;doan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every extraction now runs against these windows, never the raw page. beehiiv had the same bug — "3 months" from an unrelated widget.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lie #7: A percentage glued to the wrong noun
&lt;/h2&gt;

&lt;p&gt;swipepages.com, in a feature banner:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Upto 30% commission · 60 day cookie lifespan · &lt;strong&gt;100% timely payouts&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parser said &lt;strong&gt;100%&lt;/strong&gt;. Real rate: &lt;strong&gt;30%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;100%&lt;/code&gt; belongs to &lt;em&gt;timely payouts&lt;/em&gt; — a boast about paying on schedule. It isn't a commission rate at all. My context window was ±110 characters and the word &lt;em&gt;commission&lt;/em&gt; sat right there in the same banner, so the commission-word gate passed happily.&lt;/p&gt;

&lt;p&gt;Proximity said yes. Grammar said no. I was only checking proximity.&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="c1"&gt;// Nouns that commonly follow a percentage WITHOUT it being a commission.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NGAY_SAU_XAU&lt;/span&gt; &lt;span class="o"&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="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;timely|on&lt;/span&gt;&lt;span class="se"&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;time|secure|safe|accurate|free|money&lt;/span&gt;&lt;span class="se"&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;back|refund|satisfaction|guarantee&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;d&lt;/span&gt;&lt;span class="se"&gt;]?&lt;/span&gt;&lt;span class="sr"&gt;|uptime|automated|transparent|verified|retention|of &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;our &lt;/span&gt;&lt;span class="se"&gt;)?(?:&lt;/span&gt;&lt;span class="sr"&gt;users|customers|clients&lt;/span&gt;&lt;span class="se"&gt;))\b&lt;/span&gt;&lt;span class="sr"&gt;/i&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;ngaySau&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NGAY_SAU_XAU&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;ngaySau&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marketing banners line up unrelated numbers for visual rhythm: &lt;code&gt;30% commission&lt;/code&gt;, &lt;code&gt;60 day cookie&lt;/code&gt;, &lt;code&gt;100% timely payouts&lt;/code&gt;. Three different units in one row, with no grammar tying any of them together. A window-based extractor sees one soup of tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #8: Lie #3 again, pointed the other way
&lt;/h2&gt;

&lt;p&gt;fliki.ai:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;30-day cookie&lt;/strong&gt; · 30% lifetime commission&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parser said &lt;strong&gt;permanent cookie&lt;/strong&gt;. Real: &lt;strong&gt;30 days&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My lifetime-cookie pattern allowed 30 characters of slack between the two words:&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="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(?:&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;.]{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nx"&gt;lifetime&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;"cookie 30% lifetime"&lt;/code&gt; fits in that gap perfectly. But the &lt;em&gt;lifetime&lt;/em&gt; modifies &lt;strong&gt;commission&lt;/strong&gt;, not cookie — the cookie is stated as 30 days two words earlier.&lt;/p&gt;

&lt;p&gt;This is bug #3 inverted, in the same field, in the same regex. #3 &lt;strong&gt;missed&lt;/strong&gt; a permanent cookie because it trusted a number that was being negated. #8 &lt;strong&gt;invented&lt;/strong&gt; a permanent cookie because it trusted an adjective that belonged to a different noun. One regex, two opposite ways to be wrong.&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="c1"&gt;// No % between the two words, and "lifetime" must not be modifying commission.&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(?:&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;]{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nf"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;(?&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(?:&lt;/span&gt;&lt;span class="nx"&gt;recurring&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;commission&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;payout&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;revenue&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;rev&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the part that bothers me. &lt;strong&gt;Neither of these was caught by a test.&lt;/strong&gt; They were caught because the output was absurd — two programs claiming 100% commission paid forever. No business survives paying out 100% of revenue in perpetuity, so I went and read the pages.&lt;/p&gt;

&lt;p&gt;Absurdity is a weak detector. It catches &lt;code&gt;$23,940 per sale&lt;/code&gt; and &lt;code&gt;100% forever&lt;/code&gt;. It does nothing about a program that pays 25% when the page says 20%. Every wrong-but-plausible number my parser has ever emitted is still sitting in that database, and I have no way to find them except by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lie #9: Attributes merged across two different sentences
&lt;/h2&gt;

&lt;p&gt;This one I found by hand, and only because the number was absurd.&lt;/p&gt;

&lt;p&gt;purevpn.com's page contains these two statements, in different sentences, in different sections:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our monthly plan brings you a &lt;strong&gt;100% flat commission&lt;/strong&gt;, while all other plans get 40%&lt;/p&gt;

&lt;p&gt;Flat &lt;strong&gt;35% recurring commission for lifetime&lt;/strong&gt; on all plans.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My parser reported: &lt;strong&gt;100%, lifetime.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That program does not exist. The &lt;code&gt;100%&lt;/code&gt; is a one-time bounty on the monthly plan. The &lt;em&gt;lifetime&lt;/em&gt; term pays &lt;strong&gt;35%&lt;/strong&gt;. The parser took the percentage from sentence A and the payout type from sentence B and stapled them together into terms no page ever offered.&lt;/p&gt;

&lt;p&gt;The cause was my own fix from lie #6. I had narrowed extraction to ±130-character windows around commission keywords — which solved the earnings-calculator problem, but a 130-character window still straddles sentence boundaries. Inside one window, two unrelated claims look like one claim.&lt;/p&gt;

&lt;p&gt;My first fix was to make the sentence the unit of extraction instead of the window. That failed, and the way it failed is the interesting part.&lt;/p&gt;

&lt;p&gt;The real page text is a &lt;strong&gt;single sentence&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our monthly plan brings you a 100% flat commission, while all other plans get you 40%, on top of it a 35% recurring commission for lifetime on all plans.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three different commission rates, comma-separated, one sentence. Sentence scope put all three in the same bucket and the largest won again. I got &lt;code&gt;100%&lt;/code&gt;. Then I scoped tighter — proximity only, a window right around the number — and got &lt;code&gt;40%&lt;/code&gt;, because &lt;code&gt;40%, on top of it a 35% recurring&lt;/code&gt; puts &lt;em&gt;recurring&lt;/em&gt; within sixty characters of the 40.&lt;/p&gt;

&lt;p&gt;What finally worked wasn't a smaller window. It was a rule about what lies between:&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;KY_HAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/recurring|for life|lifetime|every month|month after month/i&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;sauDo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If another % sits between this number and the term keyword,&lt;/span&gt;
&lt;span class="c1"&gt;// the keyword belongs to THAT number, not this one.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dinhSau&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;KY_HAN&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;sauDo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sauDo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KY_HAN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sauDo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An intervening percentage sign is the signal. Not distance — &lt;em&gt;interruption&lt;/em&gt;. If another number stands between this number and the word &lt;em&gt;recurring&lt;/em&gt;, then &lt;em&gt;recurring&lt;/em&gt; is describing the other one.&lt;/p&gt;

&lt;p&gt;purevpn: &lt;code&gt;100%&lt;/code&gt; → another &lt;code&gt;%&lt;/code&gt; before any keyword → rejected. &lt;code&gt;40%&lt;/code&gt; → another &lt;code&gt;%&lt;/code&gt; before the keyword → rejected. &lt;code&gt;35%&lt;/code&gt; → keyword first → accepted. &lt;strong&gt;35% lifetime.&lt;/strong&gt; Correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  And then I wrote the tests I should have written first
&lt;/h3&gt;

&lt;p&gt;Nine bugs in, I finally did the obvious thing: pinned all ten known cases into a test file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ purevpn (real page text)    ✓ circle
✓ systeme                     ✓ scalenut
✓ kartra                      ✓ snov
✓ lemlist                     ✓ hunter
✓ swipepages                  ✓ fliki
All 10 cases pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It paid for itself on the first run. The proximity fix I was about to ship broke hunter.io — &lt;code&gt;Earn $27.93 per referral every month. Refer 10 people and get $297.30&lt;/code&gt; is two sentences, and my aggregate-rejection guard (&lt;code&gt;refer \d+&lt;/code&gt;) was reading the second one and throwing away the correct number in the first. Exactly bug #9 again, in a different extractor, and I would have shipped it.&lt;/p&gt;

&lt;p&gt;Nine bugs found by reading output. One found by a test, three minutes after the test existed.&lt;/p&gt;

&lt;p&gt;Notice the shape of this: &lt;strong&gt;fix #6 caused bug #9.&lt;/strong&gt; Narrowing the scan from the page to a window fixed one class of error and created another. The window was the wrong abstraction — close enough to look right for 150 programs, wrong enough to invent terms for one of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What hand-checking actually found
&lt;/h3&gt;

&lt;p&gt;I took the top four lifetime programs and read their pages myself. Not a sample — the top four, because those are the ones anyone would act on first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Parser said&lt;/th&gt;
&lt;th&gt;Page said&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;purevpn.com&lt;/td&gt;
&lt;td&gt;100% lifetime&lt;/td&gt;
&lt;td&gt;35% lifetime&lt;/td&gt;
&lt;td&gt;✗ wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;systeme.io&lt;/td&gt;
&lt;td&gt;60% lifetime&lt;/td&gt;
&lt;td&gt;60% lifetime&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scalenut.com&lt;/td&gt;
&lt;td&gt;50% lifetime&lt;/td&gt;
&lt;td&gt;"30%–50%" — range&lt;/td&gt;
&lt;td&gt;✗ overstated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;circle.so&lt;/td&gt;
&lt;td&gt;30% lifetime&lt;/td&gt;
&lt;td&gt;30% lifetime&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Two of four wrong at the top of the ranking.&lt;/strong&gt; One invented, one quietly taking the best end of a range nobody reaches.&lt;/p&gt;

&lt;p&gt;Both are now fixed, and the ranges resolve to their lower bound. But I want to be precise about what that does and does not mean: I fixed the two errors I found by reading four pages. There are 151 programs in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the data actually says
&lt;/h2&gt;

&lt;p&gt;After all six fixes, cross-checked against five programs I'd read by hand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payout structure, n=151&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Doesn't disclose&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurring, unspecified duration&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed term (mostly 12 months)&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lifetime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;17&lt;/strong&gt; &lt;del&gt;19&lt;/del&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 17 lifetime programs, by headline rate: systeme.io 60% (permanent cookie), moosend 40%, purevpn 35%, then a cluster at 30% — thinkific, vidyo.ai, swipepages, mailerlite, circle.so, fliki, koala.sh, scalenut — down to kinsta at 10%.&lt;/p&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Recurring" usually means twelve months.&lt;/strong&gt; It reads like forever. Of the 30 fixed-term programs nearly all cap at 12, and several bury that in an FAQ while the hero section says &lt;em&gt;recurring&lt;/em&gt; unqualified. Kartra is the clearest case: the headline says "recurring", the FAQ says "We do not offer lifetime commissions."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;38% disclose nothing.&lt;/strong&gt; 58 of 151 state a percentage and stop. Not fine print — absent. Whether it recurs, for how long, what the cookie window is: you find out after you sign up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The headline rate is the least reliable field on the page.&lt;/strong&gt; It's in the largest type and it's the one most often qualified into something else by a clause elsewhere — &lt;code&gt;up to&lt;/code&gt;, &lt;code&gt;first 12 months&lt;/code&gt;, &lt;code&gt;tier 3&lt;/code&gt;, &lt;code&gt;first payment only&lt;/code&gt;, or a range whose top end almost nobody reaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Networks:&lt;/strong&gt; PartnerStack 37, Impact 23, FirstPromoter 11, Awin 5, Tapfiliate 4, then a long tail. PartnerStack's dominance in SaaS is real and it is not close.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I couldn't reach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;281 of 554 returned 404&lt;/strong&gt; on every probed path. Either no public program, or it's gated behind a form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61 returned 403&lt;/strong&gt; — Cloudflare bot protection. Recorded as &lt;code&gt;403&lt;/code&gt; and left alone. The tool doesn't rotate user agents, solve challenges, or pretend to be a browser. If a site says no, that's a data point, not an obstacle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;19 had a page but nothing extractable.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The terms that matter most aren't on the page anyway.&lt;/strong&gt; Which countries can withdraw, tax handling, approval time, clawback windows — mostly visible only from inside the dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The crawler also killed itself
&lt;/h3&gt;

&lt;p&gt;Worth mentioning, since it cost me a full run. At domain 454 of 554, the process died:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SocketError: other side closed
    at TLSSocket.onHttp2SocketEnd (node:internal/deps/undici/undici:8758:26)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A server half-closed an HTTP/2 connection and undici emitted &lt;code&gt;error&lt;/code&gt; on an internal stream — &lt;strong&gt;outside&lt;/strong&gt; the promise chain. &lt;code&gt;try/catch&lt;/code&gt; around &lt;code&gt;await fetch()&lt;/code&gt; cannot catch that, and an unhandled &lt;code&gt;error&lt;/code&gt; event takes the whole process with it.&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncaughtException&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;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;loiNgoai&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  ! out-of-band error #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;loiNgoai&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (skipped): &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crude, and I would not ship it in a service that writes anything. In a read-only crawler it's the right trade. The final run swallowed &lt;strong&gt;44&lt;/strong&gt; of these — 44 chances to lose twenty minutes of work.&lt;/p&gt;

&lt;p&gt;Everything survived only because the database is written per-domain as the scan goes, not at the end. That was luck, not foresight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I keep coming back to
&lt;/h2&gt;

&lt;p&gt;Every one of the nine bugs is the same bug.&lt;/p&gt;

&lt;p&gt;The page says one thing. The machine reads another. Not because the regex was weak — because &lt;strong&gt;these pages are written to sell, not to be read.&lt;/strong&gt; The negation that reverses the cookie. The adjective that outranks the number. The calculator that fills the DOM with plausible decoys. The &lt;code&gt;100%&lt;/code&gt; that turns out to describe how &lt;em&gt;punctually&lt;/em&gt; they pay. The $23,940 that exists only inside a marketing screenshot.&lt;/p&gt;

&lt;p&gt;None of that is adversarial. Nobody at lemlist built an earnings slider to break my scraper. It's just that persuasive writing and machine-readable writing pull in opposite directions, and every page I hit was optimized for the first one.&lt;/p&gt;

&lt;p&gt;So the rule I ended with: &lt;strong&gt;the crawler narrows 554 down to 151. It does not tell you the truth.&lt;/strong&gt; Before any of these numbers goes anywhere that matters, open the vendor's page and read it yourself. I fixed six of these before I trusted the output. Then a wider scan handed me two more, reading four pages by hand handed me a ninth, and the test suite I should have written on day one caught a tenth within minutes of existing.&lt;/p&gt;

&lt;p&gt;Nine of those ten were found by a human reading output and thinking &lt;em&gt;that can't be right&lt;/em&gt;. If you take one thing from this post, take that: a scraper's failure mode is not crashing. It's returning a number.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;707 lines across four files, Node standard library only. No affiliate links in this post — I'm not enrolled in any of these programs, and the rankings are mechanical output, not endorsements. Numbers verified against live pages on the day of publication; vendors change terms without notice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>node</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
