<?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: Mohamed Zaghloul</title>
    <description>The latest articles on DEV Community by Mohamed Zaghloul (@mowa).</description>
    <link>https://dev.to/mowa</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%2F3245691%2Fc4014f38-8f9c-4c4a-8b86-89ccb22c56e5.jpg</url>
      <title>DEV Community: Mohamed Zaghloul</title>
      <link>https://dev.to/mowa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mowa"/>
    <language>en</language>
    <item>
      <title>Companies that sponsored green cards for frontend developers in 2026 (free list from DOL PERM data)</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:34:08 +0000</pubDate>
      <link>https://dev.to/mowa/companies-that-sponsored-green-cards-for-frontend-developers-in-2026-free-list-from-dol-perm-data-5fji</link>
      <guid>https://dev.to/mowa/companies-that-sponsored-green-cards-for-frontend-developers-in-2026-free-list-from-dol-perm-data-5fji</guid>
      <description>&lt;p&gt;If you're a frontend developer on a work visa, "does this company sponsor green cards?" is one of the hardest questions to answer from a job post. Most listings say nothing, and a recruiter's "we support visas" can mean H-1B only.&lt;/p&gt;

&lt;p&gt;There's a better signal: the US Department of Labor publishes every PERM labor certification decision. PERM is the step most employer-sponsored green cards go through, so if a company filed PERM for a frontend role this year, it has actually done the paperwork before.&lt;/p&gt;

&lt;p&gt;I went through the latest DOL PERM disclosure file (FY2026 Q3, decisions from Oct 1, 2025 to Jun 30, 2026) and pulled every case with a frontend-style job title (front end, frontend, front-end, UI developer, UI engineer, React developer). That's &lt;strong&gt;151 cases at 103 employers&lt;/strong&gt;. Here are the top 15.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free sample: top 15 frontend green card sponsors (FY2026)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Employer&lt;/th&gt;
&lt;th&gt;PERM cases&lt;/th&gt;
&lt;th&gt;Certified&lt;/th&gt;
&lt;th&gt;Median offered wage&lt;/th&gt;
&lt;th&gt;Top worksite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ByteDance Inc.&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;$177,440&lt;/td&gt;
&lt;td&gt;San Jose, CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TikTok Inc.&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$184,662&lt;/td&gt;
&lt;td&gt;San Jose, CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FMR LLC (Fidelity)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$161,363&lt;/td&gt;
&lt;td&gt;Westlake, TX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U.S. Bank National Association&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$142,522&lt;/td&gt;
&lt;td&gt;Irving, TX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IBM Corporation&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$153,807&lt;/td&gt;
&lt;td&gt;San Jose, CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interglobe IT LLC&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$105,997&lt;/td&gt;
&lt;td&gt;Irving, TX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Milwaukee Electric Tool Corporation&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$100,500&lt;/td&gt;
&lt;td&gt;Brookfield, WI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snap Finance, LLC.&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$120,000&lt;/td&gt;
&lt;td&gt;West Valley City, UT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TruStartIT LLC&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$87,610&lt;/td&gt;
&lt;td&gt;Richardson, TX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advantis Global, LLC&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$164,320&lt;/td&gt;
&lt;td&gt;Mill Valley, CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apptegy, Inc.&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$62,920&lt;/td&gt;
&lt;td&gt;Little Rock, AR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AvidXchange, Inc.&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$144,900&lt;/td&gt;
&lt;td&gt;Charlotte, NC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Barclays Services Corp.&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$175,000&lt;/td&gt;
&lt;td&gt;Whippany, NJ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOGIWARE, INC.&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$83,325&lt;/td&gt;
&lt;td&gt;Alpharetta, GA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purchasing Power LLC&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$142,002&lt;/td&gt;
&lt;td&gt;Atlanta, GA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: DOL OFLC PERM disclosure data, FY2026 Q3. Ties at 2 cases are shown in file order; more employers have 2 cases than fit here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Get the full list free: &lt;a href="https://mohabaha0.gumroad.com/l/green-card-sponsors-fy2026" rel="noopener noreferrer"&gt;Frontend &amp;amp; Software Green Card Sponsors FY2026&lt;/a&gt;&lt;/strong&gt; (pay what you want, $0 is fine). It has all 103 frontend employers plus a software developer tab with the top 1,150 sponsors, as a Google Sheet and CSV files.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read the columns
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PERM cases&lt;/strong&gt;: how many PERM applications DOL decided for that employer with a frontend-style title in this window. It counts applications, not people hired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certified&lt;/strong&gt;: how many DOL approved (this includes "certified-expired", meaning it was approved but the next step wasn't filed in time). Across all 151 frontend cases, 146 were certified, 3 denied and 2 withdrawn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median offered wage&lt;/strong&gt;: the middle of the yearly wages the employer wrote on those applications. It's what they offered for that specific role and location, not a company-wide salary band.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top worksite&lt;/strong&gt;: where the job actually is. This often differs from headquarters (Fidelity is based in Boston, but most of its cases here are in Westlake, TX).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list also has the top job titles per employer and the date of the latest decision, which tells you how recently they filed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use the list in a job search
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with your location or the places you'd move to.&lt;/strong&gt; Filter the sheet by worksite. A company with three cases in your city is a better lead than a famous name that only sponsored in another state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the job titles.&lt;/strong&gt; Many cases are senior, lead or principal roles. If an employer only filed for "Principal Front End Engineer", that tells you the level they're willing to sponsor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check their careers page.&lt;/strong&gt; Search for frontend, UI or React openings. Past filings don't mean they're hiring now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask early, and ask the right question.&lt;/strong&gt; "Do you sponsor H-1B?" and "Do you sponsor green cards (PERM)?" are different questions. Ask both in your first recruiter call so neither of you wastes time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track who said what.&lt;/strong&gt; Once you're talking to 20+ companies, it's easy to forget which one said "case by case". I keep mine in a simple sheet (&lt;a href="https://mohabaha0.gumroad.com/l/job-application-tracker" rel="noopener noreferrer"&gt;Job Application Tracker&lt;/a&gt;), but any spreadsheet works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If frontend roles are too narrow for you, the software developer tab is much bigger: DOL decided &lt;strong&gt;25,078&lt;/strong&gt; PERM cases for software developers and web developers (SOC codes 15-1252 and 15-1254) at &lt;strong&gt;5,188 employers&lt;/strong&gt; in the same window. The list covers the top &lt;strong&gt;1,150&lt;/strong&gt; of them by case count (each with 4 or more cases, 19,254 cases in total). That's not every sponsor: the other 4,038 employers, the smaller filers, aren't in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits (please read before you rely on this)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is PERM, not H-1B.&lt;/strong&gt; A company can file lots of H-1Bs and few or no PERM applications, or the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small numbers.&lt;/strong&gt; Most frontend employers here (82 of 103) had a single case. A "100% approval rate" on 1–3 cases means very little.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Title matching isn't perfect.&lt;/strong&gt; I matched job titles, not job descriptions. Some titles are senior or managerial, and a few aren't web work at all (Milwaukee Tool's cases are for a "Senior Concept Design Engineer – Front End Innovation", which reads like a product design role). Check the titles in the full list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staffing and IT consulting firms appear.&lt;/strong&gt; Some employers place workers at client sites. That's a legitimate route for some people, but it's a different job than working in-house, so look them up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Past filings don't promise future sponsorship.&lt;/strong&gt; Budgets, policies and headcount change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data lags.&lt;/strong&gt; DOL publishes disclosure files quarterly, and this one ends on Jun 30, 2026. Cases decided since then aren't in it yet. The raw files are public on the &lt;a href="https://flag.dol.gov/programs/perm" rel="noopener noreferrer"&gt;DOL FLAG site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not legal advice.&lt;/strong&gt; For your own case, talk to an immigration attorney.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I left employer tax IDs out of the list on purpose. Everything else comes straight from the public DOL file.&lt;/p&gt;

&lt;h2&gt;
  
  
  For developers: pull your own fresh list
&lt;/h2&gt;

&lt;p&gt;If you'd rather query the data yourself, by job title, city or salary, I published the tool I used to build this list: &lt;a href="https://apify.com/chimey_forearm/perm-green-card-sponsors" rel="noopener noreferrer"&gt;PERM Green Card Sponsors on Apify&lt;/a&gt;. It reads the DOL disclosure files directly and groups cases by employer. Most job seekers won't need it; the sheet is easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the full list
&lt;/h2&gt;

&lt;p&gt;The free sample above is 15 of 103 frontend sponsors. &lt;strong&gt;&lt;a href="https://mohabaha0.gumroad.com/l/green-card-sponsors-fy2026" rel="noopener noreferrer"&gt;Download the full Frontend &amp;amp; Software Green Card Sponsors FY2026 list&lt;/a&gt;&lt;/strong&gt;: pay what you want, including $0.&lt;/p&gt;

&lt;p&gt;If you spot an employer that's mislabeled or a mistake in the numbers, tell me in the comments and I'll fix it.&lt;/p&gt;

</description>
      <category>immigration</category>
      <category>career</category>
      <category>jobs</category>
      <category>frontend</category>
    </item>
    <item>
      <title>How to find government RFPs in 549 city/county portals for free</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Mon, 19 Oct 2026 19:42:29 +0000</pubDate>
      <link>https://dev.to/mowa/how-to-find-government-rfps-in-549-citycounty-portals-for-free-33g8</link>
      <guid>https://dev.to/mowa/how-to-find-government-rfps-in-549-citycounty-portals-for-free-33g8</guid>
      <description>&lt;p&gt;If you sell to local government (IT services, construction, janitorial, consulting), you know the annoying part of the job isn't writing the proposal. It's finding the bid in time. Every city, county, school district and transit agency posts RFPs on its own portal, and paid bid-alert services that watch them for you &lt;a href="https://jorpex.com/compare/bidnet-direct-alternatives" rel="noopener noreferrer"&gt;typically run $49 to $149 a month&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A big chunk of those agencies use the same e-procurement platform, OpenGov Procurement. Right now its public agency directory lists &lt;strong&gt;549 active agencies&lt;/strong&gt; across the US (127 in California, 93 in Florida, 49 in Arizona, 44 in Texas...). So I built an independent Apify Actor that checks all of them for you: &lt;a href="https://apify.com/chimey_forearm/opengov-bids-scraper" rel="noopener noreferrer"&gt;OpenGov Procurement Bids Scraper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it free:&lt;/strong&gt; open the &lt;a href="https://apify.com/chimey_forearm/opengov-bids-scraper/examples/find-software-and-it-bids-in-florida-government-portals" rel="noopener noreferrer"&gt;Florida software/IT bids example&lt;/a&gt; and press Start. Apify's free plan includes $5 of platform usage every month and doesn't need a credit card, which covers about 2,500 bids at this Actor's price.&lt;/p&gt;

&lt;p&gt;This post walks through two real runs I did today: what goes in, what comes out, how the "only new bids" mode works, and what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scan a whole state in one run.&lt;/strong&gt; Put &lt;code&gt;"FL"&lt;/code&gt; in &lt;code&gt;states&lt;/code&gt; and every active OpenGov Procurement agency in Florida is checked, using OpenGov's own agency directory. No list of portal slugs to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only-new mode for a real alert feed.&lt;/strong&gt; With &lt;code&gt;onlyNew&lt;/code&gt; on, each run returns only bids you haven't received yet. If an agency posts an addendum to a bid you already have, that bid comes back once with &lt;code&gt;changeType: "new_addendum"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The details you actually bid with.&lt;/strong&gt; Buyer name, title, email and phone (as the agency published them), Q&amp;amp;A deadline, pre-bid meeting, UNSPSC/NIGP category codes, addenda, attachment names, timeline and evaluation criteria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$2 per 1,000 bids&lt;/strong&gt;, details included. Other bid Actors on the Apify Store I compared charge $3 to $20 per 1,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Input
&lt;/h2&gt;

&lt;p&gt;This is the exact input I ran:&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;"states"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"software"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"technology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cybersecurity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IT services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywordsInTitleOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"onlyNew"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stateKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devto-demo-fl-it"&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;ul&gt;
&lt;li&gt;
&lt;code&gt;states&lt;/code&gt; scans every active OpenGov agency in those states. You can also pass &lt;code&gt;agencies&lt;/code&gt; (portal codes like &lt;code&gt;pittsburghpa&lt;/code&gt; or full portal URLs) instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keywords&lt;/code&gt; match the start of words, case-insensitive. &lt;code&gt;keywordsInTitleOnly&lt;/code&gt; keeps it strict, so "cloud" doesn't match "St. Cloud" in a summary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onlyNew&lt;/code&gt; remembers what you already got, under the name in &lt;code&gt;stateKey&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other useful filters: &lt;code&gt;dueWithinDays&lt;/code&gt;, &lt;code&gt;postedWithinDays&lt;/code&gt;, &lt;code&gt;excludeKeywords&lt;/code&gt;, &lt;code&gt;solicitationTypes&lt;/code&gt; (e.g. "Request for Proposal"), &lt;code&gt;agencyNameContains&lt;/code&gt; (e.g. "school", "airport") and &lt;code&gt;statuses&lt;/code&gt; (open by default).&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Agencies scanned&lt;/th&gt;
&lt;th&gt;Open bids listed&lt;/th&gt;
&lt;th&gt;Matches returned&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First run&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;324&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 new bids&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-run right after&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;324&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; (4 skipped as already seen)&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both ran at 256 MB, and peak memory was about 60 MB. The four Florida matches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agency&lt;/th&gt;
&lt;th&gt;Bid&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;City of Clermont&lt;/td&gt;
&lt;td&gt;Cloud Based Location Intelligence Foot Traffic Data Platform&lt;/td&gt;
&lt;td&gt;Request for Proposal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;City of Lakeland&lt;/td&gt;
&lt;td&gt;Consulting Services for the Preparation of a Data Center and Large-Load Development Regulatory Framework&lt;/td&gt;
&lt;td&gt;Request for Proposal - Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jacksonville Transportation Authority&lt;/td&gt;
&lt;td&gt;Public Engagement Web Based Software Platform&lt;/td&gt;
&lt;td&gt;Solicitation (Attach a Document)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Putnam County School District&lt;/td&gt;
&lt;td&gt;K-12 Master Scheduling Software, Implementation, and Support Services&lt;/td&gt;
&lt;td&gt;Request for Proposal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That second run is the point. Schedule the same input daily and most days you get zero or a handful of records, and every one is something you haven't seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output
&lt;/h2&gt;

&lt;p&gt;One dataset record per bid. Here's the Clermont one, trimmed (summary shortened, 3 of 7 evaluation criteria kept):&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cloud Based Location Intelligence Foot Traffic Data Platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"referenceNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RFP 27-002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"solicitationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request for Proposal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"City of Clermont"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agencyState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Parks and Recreation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-10T04:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dueAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-12T19:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"daysUntilDue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The City is seeking proposals from qualified firms to provide a cloud-based location intelligence and foot traffic analytics platform..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://procurement.opengov.com/portal/clermontfl/projects/298812"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"procurementContact"&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;"Freddy Suarez"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Procurement Services Director"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fsuarez@clermontfl.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(352) 241-7350"&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;"preProposalMeeting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questionsDeadline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-04T20:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sealedBid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20949"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Geographic Information System (GIS) Software"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"92005"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application, Infrastructure, Hosting and Cloud Computing Services"&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;"addendaCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attachments"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenGov Vendor Registration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pdf"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cost Proposal Form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docx"&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;"evaluationCriteria"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Proposed Platform and Technical Capabilities"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Data Methodology and Quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cost Proposal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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;"changeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new"&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 contact shown is the buyer's published work contact on the public bid page. If an agency hides its contact, the field stays empty. The Lakeland bid in the same run came back with 5 addenda, 8 attachment names and 9 category codes. Note its deadline was the same afternoon, which is exactly why a daily check beats a weekly one.&lt;/p&gt;

&lt;p&gt;Every run also writes a &lt;code&gt;RUN_SUMMARY&lt;/code&gt; record (agencies scanned, bids listed, matched, skipped as already seen, errors), so you can tell "nothing new" apart from "something broke".&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn it into a daily bid alert
&lt;/h2&gt;

&lt;p&gt;Everything below is built into Apify; nothing extra to install:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Actor, paste your input and click &lt;strong&gt;Save as a new task&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On the task, add a &lt;strong&gt;Schedule&lt;/strong&gt; (for example, every weekday at 7 AM in your time zone).&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Integrations&lt;/strong&gt;, pick where results go: Slack, Gmail or Google Drive are native options, or use a webhook, n8n, Make or Zapier to push new bids into a CRM or a spreadsheet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because &lt;code&gt;onlyNew&lt;/code&gt; is on, each scheduled run sends only new bids and new addenda, not the same list every morning. For a California construction version, see the &lt;a href="https://apify.com/chimey_forearm/opengov-bids-scraper/examples/daily-alert-for-new-california-construction-bids" rel="noopener noreferrer"&gt;daily CA construction alert example&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;It's pay-per-result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$2.00 per 1,000 bids&lt;/strong&gt; ($0.002 a bid), details included, plus a $0.00005 start fee per run&lt;/li&gt;
&lt;li&gt;Agencies with no matching bids add nothing to the bill&lt;/li&gt;
&lt;li&gt;A re-run with nothing new costs only the start fee&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first run above (4 bids) cost about $0.008. A daily Florida IT alert with a few new bids a week stays well under the $5 monthly credit on Apify's free plan. You can also set a max cost per run, and the Actor stops cleanly when it reaches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's an independent tool&lt;/strong&gt;, not made by or affiliated with OpenGov.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It covers agencies on OpenGov Procurement only.&lt;/strong&gt; Lots of agencies use other portals (Bonfire, PlanetBids, BidNet, state systems), and those aren't in here. 549 agencies is a big slice of US local government, not all of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It returns notice details with a link back&lt;/strong&gt; to each bid on the agency's portal. It doesn't download the bid documents: you get attachment titles and file types, and you download the files (and register to respond) on the agency's own page.&lt;/li&gt;
&lt;li&gt;Keyword matching is simple word-start matching. Good keywords and &lt;code&gt;excludeKeywords&lt;/code&gt; matter more than anything else for useful results.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Store page: &lt;a href="https://apify.com/chimey_forearm/opengov-bids-scraper" rel="noopener noreferrer"&gt;OpenGov Procurement Bids Scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;One-click example: &lt;a href="https://apify.com/chimey_forearm/opengov-bids-scraper/examples/find-software-and-it-bids-in-florida-government-portals" rel="noopener noreferrer"&gt;Florida software and IT bids&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free to try on Apify's free plan ($5 of monthly credit, no credit card)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's new, so I'd really like feedback. If a field you need when bidding is missing, or a keyword setup gives you noise, tell me in the comments or open an issue on the Actor page.&lt;/p&gt;

</description>
      <category>government</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Every ID on this invoice is valid. Here's why you shouldn't pay it (MCP payee check)</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:30:23 +0000</pubDate>
      <link>https://dev.to/mowa/every-id-on-this-invoice-is-valid-heres-why-you-shouldnt-pay-it-mcp-payee-check-c8a</link>
      <guid>https://dev.to/mowa/every-id-on-this-invoice-is-valid-heres-why-you-shouldnt-pay-it-mcp-payee-check-c8a</guid>
      <description>&lt;p&gt;Every number on this invoice is valid. You still shouldn't pay it.&lt;/p&gt;

&lt;p&gt;Here's an invoice from a German supplier: German VAT number, German BIC, and an IBAN that passes its mod-97 checksum. A normal IBAN or VAT validator will say all three are valid. But that IBAN belongs to a Dutch bank, so the invoice is asking you to pay a German supplier into a Dutch account. That's what changed-bank-details fraud usually looks like: a real supplier, real-looking IDs, and an account that doesn't fit.&lt;/p&gt;

&lt;p&gt;I added a tool to my invoice validator MCP server that checks for exactly this. It's called &lt;code&gt;check_invoice_payee&lt;/code&gt;, and it returns one verdict: &lt;code&gt;pay&lt;/code&gt;, &lt;code&gt;review&lt;/code&gt; or &lt;code&gt;hold&lt;/code&gt;. Here's what it gives back for that invoice. This is real output from the free demo endpoint, trimmed for length:&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;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"risk_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hold payment: BIC is from DE but the IBAN is from NL: the two don't belong to the same bank account; Supplier is in DE but wants payment to a NL bank account: the classic changed-bank-details fraud pattern. Confirm by phone using a known number."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reasons"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bic_iban_country_mismatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BIC is from DE but the IBAN is from NL: the two don't belong to the same bank account"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iban_country_differs_from_supplier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Supplier is in DE but wants payment to a NL bank account: the classic changed-bank-details fraud pattern. Confirm by phone using a known number"&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;"checks"&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;"iban"&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;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sepa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"formatted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NL91 ABNA 0417 1643 00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bank_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABNA"&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;"bic"&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;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bank_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COBA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matches_iban_country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"country_mismatch_with_iban"&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;"vat"&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;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu_vat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE136695976"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;checks.iban.valid&lt;/code&gt;: it's &lt;code&gt;true&lt;/code&gt;. A per-field validator stops there. What matters is comparing the fields against each other.&lt;/p&gt;

&lt;p&gt;Swap in a German IBAN (&lt;code&gt;DE89370400440532013000&lt;/code&gt;) and keep everything else the same, and you get:&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="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"risk_score"&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="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"All identifiers are valid and consistent with each other. OK to pay."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reasons"&lt;/span&gt;&lt;span class="p"&gt;:[]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it cross-checks
&lt;/h2&gt;

&lt;p&gt;You send whatever the invoice has (&lt;code&gt;iban&lt;/code&gt;, &lt;code&gt;bic&lt;/code&gt;, &lt;code&gt;vat&lt;/code&gt;, &lt;code&gt;ein&lt;/code&gt;), plus &lt;code&gt;supplier_country&lt;/code&gt;, and optionally &lt;code&gt;supplier_name&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt; and &lt;code&gt;live&lt;/code&gt;. It then checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IBAN vs supplier country.&lt;/strong&gt; A German supplier asking to be paid into a Dutch or Lithuanian account gets flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BIC vs IBAN.&lt;/strong&gt; Both have to point at the same country, or they can't be the same account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VAT ownership.&lt;/strong&gt; With &lt;code&gt;live: true&lt;/code&gt;, the company name EU VIES has on file is compared with &lt;code&gt;supplier_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The smaller stuff:&lt;/strong&gt; unregistered VAT numbers, test BICs, and EUR invoices paid into non-SEPA accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each problem comes back as a &lt;code&gt;reasons[]&lt;/code&gt; entry with a machine-readable &lt;code&gt;code&lt;/code&gt; and a plain-English message, so an agent can act on it and a person can read it.&lt;/p&gt;

&lt;p&gt;What it doesn't do: it checks formats, checksums, how the IDs relate to each other, and (with &lt;code&gt;live&lt;/code&gt;) VIES. It can't prove a bank account belongs to a particular company. Nothing short of the bank can. A &lt;code&gt;hold&lt;/code&gt; means "call the supplier on a number you already have", not "this is fraud".&lt;/p&gt;

&lt;h2&gt;
  
  
  Add it to Claude Desktop or Cursor
&lt;/h2&gt;

&lt;p&gt;The server is hosted (remote, Streamable HTTP), so there's nothing to install. For the free demo (10 tool calls per IP per day, no key), add this to your MCP config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice-validator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cursor that goes in &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;. In Claude (desktop or web), go to Settings → Connectors → Add custom connector and paste the URL. If your client only supports stdio servers, including Claude Desktop's &lt;code&gt;claude_desktop_config.json&lt;/code&gt;, bridge it with mcp-remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice-validator"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/mcp"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just ask: &lt;em&gt;"Check this payee before I pay it: supplier in Germany, IBAN NL91ABNA0417164300, BIC COBADEFFXXX, VAT DE136695976."&lt;/em&gt; The model calls &lt;code&gt;check_invoice_payee&lt;/code&gt; and you get the verdict and the reasons.&lt;/p&gt;

&lt;p&gt;For production use, the same server runs behind RapidAPI (the free Basic plan gives 100 requests a month):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice-validator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://invoice-payment-id-validator.p.rapidapi.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"X-RapidAPI-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_RAPIDAPI_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"X-RapidAPI-Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice-payment-id-validator.p.rapidapi.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Or just call it over plain HTTP
&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 https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/v1/payee-check &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;'{"supplier_country":"DE","supplier_name":"Muster GmbH","iban":"NL91ABNA0417164300","bic":"COBADEFFXXX","vat":"DE136695976","currency":"EUR"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The paid route is &lt;code&gt;POST /v1/payee-check&lt;/code&gt; on the RapidAPI host. The server also has single-ID tools (&lt;code&gt;validate_iban&lt;/code&gt;, &lt;code&gt;validate_bic&lt;/code&gt;, &lt;code&gt;validate_eu_vat&lt;/code&gt;, &lt;code&gt;validate_gb_vat&lt;/code&gt;, &lt;code&gt;validate_ein&lt;/code&gt;) for when you only need one check.&lt;/p&gt;

&lt;p&gt;Docs and the MCP Registry manifest are at &lt;a href="https://github.com/Mowaah/invoice-validator-mcp" rel="noopener noreferrer"&gt;github.com/Mowaah/invoice-validator-mcp&lt;/a&gt;. A simple rule for an accounts-payable agent: auto-pay only on &lt;code&gt;pay&lt;/code&gt;, send &lt;code&gt;review&lt;/code&gt; to a person, and never pay on &lt;code&gt;hold&lt;/code&gt; until someone has phoned the supplier.&lt;/p&gt;

&lt;p&gt;If you'd rather click than code, there's a &lt;a href="https://invoice-validator.mohamed-bahaa53000.workers.dev/tools/iban-vat-checker" rel="noopener noreferrer"&gt;free IBAN &amp;amp; VAT checker&lt;/a&gt; in the browser, and I'll also &lt;a href="https://mohabaha0.gumroad.com/l/n8n-setup" rel="noopener noreferrer"&gt;set the whole invoice-triage workflow up in your n8n for $149&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>fintech</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I built a $3 Google Sheet that tracks every job application from Gmail automatically</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:31:24 +0000</pubDate>
      <link>https://dev.to/mowa/i-built-a-free-to-copy-google-sheet-that-tracks-every-job-application-from-gmail-automatically-kpl</link>
      <guid>https://dev.to/mowa/i-built-a-free-to-copy-google-sheet-that-tracks-every-job-application-from-gmail-automatically-kpl</guid>
      <description>&lt;p&gt;If you're applying to a lot of jobs, you know the problem: by application #60 you can't remember who you applied to, who rejected you, and who just went quiet. Spreadsheets help until you stop updating them, which for me was about day three.&lt;/p&gt;

&lt;p&gt;So I built a Google Sheet that fills itself in from Gmail.&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%2Ffq3srlvex4icro0wxafq.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%2Ffq3srlvex4icro0wxafq.png" alt="The Tracker tab, filled in from sample emails" width="800" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sample data: every company in these screenshots is fictional.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You make a copy of the sheet, click &lt;strong&gt;Job Tracker → Set up&lt;/strong&gt; once, and allow Gmail access. From then on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It reads the last 180 days of email and adds &lt;strong&gt;one row per application&lt;/strong&gt;: company, role, date applied, status, and a link back to the email.&lt;/li&gt;
&lt;li&gt;It checks again &lt;strong&gt;every morning on its own&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Statuses update when the next email arrives: &lt;strong&gt;Applied → Assessment → Interview → Offer / Rejected&lt;/strong&gt;. Statuses only move forward, so an old "thanks for applying" never overwrites a rejection.&lt;/li&gt;
&lt;li&gt;Anything still "Applied" after 21 days with no reply gets flagged &lt;strong&gt;Ghosted&lt;/strong&gt; (you can change the number).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Notes&lt;/strong&gt; column the scan never touches.&lt;/li&gt;
&lt;li&gt;Job-alert newsletters ("jobs you may like", "X is hiring") are ignored.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The dashboard
&lt;/h2&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%2F8cemrqqb8wy9v7bj6knv.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%2F8cemrqqb8wy9v7bj6knv.png" alt="Dashboard with totals, response rate and applications per week" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Totals by status, response rate, how many reached an assessment or interview, ghosted count, and applications per week.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it recognises emails
&lt;/h2&gt;

&lt;p&gt;It's rule-based, not AI. It knows the confirmation and status emails from Greenhouse, Lever, Workday, Ashby, SmartRecruiters, iCIMS, Workable, BambooHR, JazzHR, Breezy, Rippling, Dover, Wellfound, LinkedIn Easy Apply and Indeed, plus everyday phrases like "thank you for applying" and "we have decided to move forward with other candidates".&lt;/p&gt;

&lt;p&gt;I tested it on a real inbox with 200+ applications. On my labelled test emails, it got the status and company name right about 9 in 10 times, and it flagged zero newsletters or alerts as applications. The misses are mostly recruiters writing in their own words; those take a quick manual edit, or you can add a phrase in &lt;strong&gt;Settings → Extra keywords&lt;/strong&gt; and hit Scan now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;Everything runs inside your own Google account as an Apps Script attached to your copy. No AI, no API keys, no outside servers, no browser extension, no subscription. Gmail access is read-only: it never sends, changes or deletes email. You can turn the daily scan off from the menu, or remove access completely at myaccount.google.com/permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup tips (2 minutes)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open your copy in the Google account that gets your job emails.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Job Tracker → Set up (run this first)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Google will say it "hasn't verified this app". That's because the script lives in &lt;em&gt;your&lt;/em&gt; copy, not on a public server. Click &lt;strong&gt;Advanced → Go to Job Application Tracker&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On the permissions screen, tick &lt;strong&gt;Select all&lt;/strong&gt;, then &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If nothing seems to happen after allowing, click &lt;strong&gt;Job Tracker → Set up&lt;/strong&gt; once more. Google sometimes only authorizes on the first click.&lt;/li&gt;
&lt;li&gt;Big inboxes fill in chunks; the scan picks up again by itself about a minute later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gmail only, English emails only.&lt;/li&gt;
&lt;li&gt;Rule-based, so roughly 1 in 10 emails needs a manual fix.&lt;/li&gt;
&lt;li&gt;Recruiter emails written from scratch may not be recognised.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;It's a one-time payment, pay what you want from $3 (suggested $5), with no monthly plan, unlike most tracker apps. You get the copy link right after checkout:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://mohabaha0.gumroad.com/l/job-application-tracker" rel="noopener noreferrer"&gt;Job Application Tracker on Gumroad&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're in the middle of a job search, good luck out there. Questions, bugs, or an ATS it doesn't recognise? Drop a comment and I'll add it.&lt;/p&gt;

</description>
      <category>jobsearch</category>
      <category>productivity</category>
      <category>googlesheets</category>
      <category>career</category>
    </item>
    <item>
      <title>How to find visa sponsorship jobs automatically (Greenhouse, Lever and Workday boards)</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Sun, 18 Oct 2026 22:53:46 +0000</pubDate>
      <link>https://dev.to/mowa/how-to-find-visa-sponsorship-jobs-automatically-greenhouse-lever-and-workday-boards-iio</link>
      <guid>https://dev.to/mowa/how-to-find-visa-sponsorship-jobs-automatically-greenhouse-lever-and-workday-boards-iio</guid>
      <description>&lt;p&gt;If you're job hunting on an H-1B, OPT, STEM OPT or TN, you know the routine. You find a role that fits, read the whole posting, get excited, and then see the last paragraph: &lt;em&gt;"We are unable to sponsor visas for this position."&lt;/em&gt; Or &lt;em&gt;"Applicant must be a U.S. person under ITAR."&lt;/em&gt; Do that fifty times a week and it eats hours.&lt;/p&gt;

&lt;p&gt;When a company won't sponsor, that sentence is usually right there in the job description. So I built a scraper that reads it for you. This post shows how to pull jobs straight from company job boards (Greenhouse, Lever, Workday and more), flag the work-authorization wording on every posting, and drop the dead ends automatically. All the numbers below come from real runs I did on Oct 18, 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try it free:&lt;/strong&gt; &lt;a href="https://apify.com/chimey_forearm/ats-jobs-scraper" rel="noopener noreferrer"&gt;ATS Jobs Scraper on the Apify Store&lt;/a&gt;. Apify's free plan includes $5 of usage credit every month, no credit card. At $0.001 per job, that's roughly 5,000 jobs a month. There's also a &lt;a href="https://apify.com/chimey_forearm/ats-jobs-scraper/examples/scrape-workday-jobs-from-a-company-career-site" rel="noopener noreferrer"&gt;ready-made example for a Workday career site&lt;/a&gt; you can run in one click.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why company job boards, not LinkedIn
&lt;/h2&gt;

&lt;p&gt;Most tech companies post jobs through an applicant tracking system (ATS): Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee or Personio. Every one of these has a public job board, and most have a public JSON feed behind it. The Actor reads those feeds directly. It needs no browser and no login, and the full description comes with every job. The full description is the part you need, because that's where the sponsorship sentence lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this different
&lt;/h2&gt;

&lt;p&gt;Other job scrapers exist, so here's the honest comparison. The only AI-based visa sponsorship filter I found on the Apify Store covers Greenhouse and Workday only. None of the five most-used ATS job scrapers there flag US citizenship, ITAR or security clearance requirements.&lt;/p&gt;

&lt;p&gt;This one adds a &lt;code&gt;workAuthorization&lt;/code&gt; object to every job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sponsorship&lt;/code&gt;: &lt;code&gt;offered&lt;/code&gt;, &lt;code&gt;not_offered&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt; (the posting doesn't say)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usCitizenshipRequired&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usPersonRequired&lt;/code&gt; (ITAR / export rules: citizen or green card)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exportControlled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;securityClearance&lt;/code&gt;: &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;preferred&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evidence&lt;/code&gt;: the exact sentences each flag came from&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And two switches, &lt;code&gt;excludeNoSponsorship&lt;/code&gt; and &lt;code&gt;excludeCitizenshipOrClearance&lt;/code&gt;, drop matching jobs before they're saved. Dropped jobs aren't charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste input (about 10 seconds)
&lt;/h2&gt;

&lt;p&gt;Open the Actor, switch the Input tab to JSON, paste this and click &lt;strong&gt;Start&lt;/strong&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;"companies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"affirm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spacex"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ramp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"software engineer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxJobsPerCompany"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"excludeNoSponsorship"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"excludeCitizenshipOrClearance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My run of exactly this took 10 seconds. It saved 19 jobs and dropped 11. All 10 SpaceX software roles it checked need US-person status under ITAR, so SpaceX returned 0.&lt;/p&gt;

&lt;p&gt;You can give bare slugs (&lt;code&gt;stripe&lt;/code&gt;), board URLs (&lt;code&gt;https://jobs.lever.co/spotify&lt;/code&gt;) or careers pages (&lt;code&gt;https://ramp.com/careers&lt;/code&gt;). Workday always needs the full career-site URL, like &lt;code&gt;https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite&lt;/code&gt;. Slugs don't always match company names. &lt;code&gt;anduril&lt;/code&gt; came back &lt;code&gt;BOARD_NOT_FOUND&lt;/code&gt; in my test, because their Greenhouse board is &lt;code&gt;andurilindustries&lt;/code&gt;. When a slug misses, paste the board URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a flagged job looks like
&lt;/h2&gt;

&lt;p&gt;Two real records (trimmed):&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Affirm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greenhouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Senior Software Engineer, Affirm Bank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remote US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workAuthorization"&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;"sponsorship"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_offered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"usCitizenshipRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"usPersonRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"exportControlled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"securityClearance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Please note that visa sponsorship is not available for this position."&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;"applyUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://job-boards.greenhouse.io/affirm/jobs/7812982003"&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;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SpaceX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greenhouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI Security Software Engineer (Starshield)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hawthorne, CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workAuthorization"&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;"sponsorship"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"usCitizenshipRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"usPersonRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"exportControlled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"securityClearance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&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="s2"&gt;"- To conform to US Government export regulations, applicant must be a (i) US citizen or national, (ii) US lawful, permanent resident (aka green card holder), (iii) Refugee under 8 USC."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"- Active Top Secret, Top Secret SCI, or DOE Level Q clearance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ITAR REQUIREMENTS:"&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;"applyUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://boards.greenhouse.io/spacex/jobs/8880556002?gh_jid=8880556002"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Before and after the filters
&lt;/h2&gt;

&lt;p&gt;The bigger test covered Affirm, Stripe, Ramp, Databricks, SpaceX and NVIDIA's Workday site (Anduril missed, as noted above). It used keywords &lt;code&gt;"software engineer"&lt;/code&gt; and &lt;code&gt;"frontend"&lt;/code&gt; and &lt;code&gt;maxJobsPerCompany: 50&lt;/code&gt;. I ran it once with no filters and once with both filters on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Board&lt;/th&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;th&gt;No filters&lt;/th&gt;
&lt;th&gt;Both filters on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Affirm&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpaceX&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ramp&lt;/td&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA&lt;/td&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;272&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;221 (51 dropped)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the unfiltered run, all 50 SpaceX jobs were flagged US-person/ITAR, and 4 also needed a clearance. One Affirm job said no sponsorship, and 4 Affirm jobs said sponsorship &lt;em&gt;is&lt;/em&gt; offered. The other 216 jobs said nothing either way.&lt;/p&gt;

&lt;p&gt;That last number is the important lesson: &lt;strong&gt;most postings never mention sponsorship.&lt;/strong&gt; Silence doesn't mean yes, so treat &lt;code&gt;null&lt;/code&gt; as "ask the recruiter."&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep only jobs that explicitly offer sponsorship
&lt;/h2&gt;

&lt;p&gt;The filters only remove the clear "no" jobs. To keep only the clear "yes" jobs, filter the dataset on &lt;code&gt;workAuthorization.sponsorship = "offered"&lt;/code&gt;. You can do that in the dataset view, in a spreadsheet after a CSV export, or in code:&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;yes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&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;j&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workAuthorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sponsorship&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read the evidence. All 4 of Affirm's "offered" jobs in my run said: &lt;em&gt;"We are able to offer visa sponsorship for this role, but do require that someone is based in Spain for the role."&lt;/em&gt; That's real sponsorship, but for Spain, not a US H-1B. The flag can't know which country you need, and the evidence sentence can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get new jobs every day
&lt;/h2&gt;

&lt;p&gt;Two inputs turn this into a daily alert: &lt;code&gt;"onlyNewJobs": true&lt;/code&gt; and a &lt;code&gt;"stateKey"&lt;/code&gt; like &lt;code&gt;"visa-alerts"&lt;/code&gt;. Each run then returns, and charges for, only jobs it hasn't returned before. Save your input as a Task and add a daily Schedule in the Apify console. To get the results somewhere useful, use Apify's integrations on the Task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slack:&lt;/strong&gt; get a message in a channel when the run succeeds, with a link to the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email (Gmail integration):&lt;/strong&gt; send yourself an email when the run finishes, with the dataset attached as CSV or JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Sheets:&lt;/strong&gt; connect through Apify's Make, Zapier or n8n integrations, which can add each job as a row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks:&lt;/strong&gt; POST to your own endpoint when the run finishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.001 per job&lt;/strong&gt; saved, plus a &lt;strong&gt;$0.00005&lt;/strong&gt; start fee per run&lt;/li&gt;
&lt;li&gt;Jobs dropped by the work-auth filters are &lt;strong&gt;not charged&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Error records (like a board not found) count as one item each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My unfiltered run saved 273 items (272 jobs + 1 error), about $0.27. The filtered run saved 222, about $0.22. The 51 dropped jobs cost nothing. You can set a max cost per run and the Actor stops cleanly when it hits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rules, not AI.&lt;/strong&gt; Flags come from wording rules ("unable to sponsor", "must be a US citizen", "TS/SCI"). Unusual phrasing can be missed, and topic mentions can trip a flag. One NVIDIA data-governance job got &lt;code&gt;exportControlled: true&lt;/code&gt; because the description talks &lt;em&gt;about&lt;/em&gt; export controls as a product problem. Always check &lt;code&gt;evidence&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No description, no flags.&lt;/strong&gt; Jobs without description text get &lt;code&gt;workAuthorization: null&lt;/code&gt; and are never dropped. Keep &lt;code&gt;includeDescription&lt;/code&gt; on (the default).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxJobsPerCompany&lt;/code&gt; applies before the filters.&lt;/strong&gt; That's why SpaceX went from 50 to 0 instead of being refilled with other jobs. Raise the cap if you filter heavily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday lists about 2,000 jobs per site at most.&lt;/strong&gt; Add keywords for huge employers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is not legal advice.&lt;/strong&gt; A flag tells you what a posting says, not what you're eligible for. When it matters, ask the employer or an immigration attorney.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it and tell me what's wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Store page: &lt;a href="https://apify.com/chimey_forearm/ats-jobs-scraper" rel="noopener noreferrer"&gt;ATS Jobs Scraper&lt;/a&gt; (free plan: $5 credit a month, no card)&lt;/li&gt;
&lt;li&gt;One-click example: &lt;a href="https://apify.com/chimey_forearm/ats-jobs-scraper/examples/scrape-workday-jobs-from-a-company-career-site" rel="noopener noreferrer"&gt;scrape a Workday career site&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's new, so I'd really like feedback, especially from people on visas. If you find a sponsorship sentence it misses or a flag that's wrong, paste the wording in the comments or open an issue on the Actor page. Those reports are the fastest way to make the rules better.&lt;/p&gt;

</description>
      <category>career</category>
      <category>jobs</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Turn any website into clean Markdown for RAG (incremental sync, llms.txt)</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Sun, 18 Oct 2026 16:18:25 +0000</pubDate>
      <link>https://dev.to/mowa/turn-any-website-into-clean-markdown-for-rag-incremental-sync-llmstxt-2m7n</link>
      <guid>https://dev.to/mowa/turn-any-website-into-clean-markdown-for-rag-incremental-sync-llmstxt-2m7n</guid>
      <description>&lt;p&gt;If you've ever fed a website into a RAG pipeline, you know the boring part isn't the embeddings. It's getting clean text. Nav menus, cookie banners, "related posts" widgets and footers all end up in your chunks, and then every time the site changes you either re-scrape everything or guess what changed.&lt;/p&gt;

&lt;p&gt;I built a small Apify Actor for exactly this, called &lt;a href="https://apify.com/chimey_forearm/sitemap-to-markdown" rel="noopener noreferrer"&gt;Sitemap to Markdown&lt;/a&gt;. This post walks through a real run on the htmx essays: what goes in, what comes out, how re-runs only pick up changes, and what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: use the sitemap, not a crawler
&lt;/h2&gt;

&lt;p&gt;Most sites already publish a list of every page and when it last changed: &lt;code&gt;sitemap.xml&lt;/code&gt;. So instead of crawling links, the Actor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the sitemap on its own (robots.txt, &lt;code&gt;/sitemap.xml&lt;/code&gt;, sitemap indexes, &lt;code&gt;.xml.gz&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Keeps only the URLs under the path you gave it&lt;/li&gt;
&lt;li&gt;Downloads each page with plain HTTP (no headless browser)&lt;/li&gt;
&lt;li&gt;Strips the boilerplate and converts the main content to Markdown&lt;/li&gt;
&lt;li&gt;Optionally splits it into RAG-sized chunks and writes &lt;code&gt;llms.txt&lt;/code&gt; / &lt;code&gt;llms-full.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a site has no sitemap, it falls back to a shallow same-site link crawl.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input
&lt;/h2&gt;

&lt;p&gt;This is the exact input I ran:&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;"startUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://htmx.org/essays"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"incremental"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"incrementalStateKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devto-demo-htmx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunkSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createLlmsTxt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;maxPages: 0&lt;/code&gt; means no limit: every essay URL in the sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;incremental: true&lt;/code&gt; makes the Actor remember what it has seen, under the name in &lt;code&gt;incrementalStateKey&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chunkSize: 400&lt;/code&gt; splits each page into chunks of about 400 tokens, by heading and paragraph.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Output
&lt;/h2&gt;

&lt;p&gt;You get one dataset record per page. Each page's &lt;code&gt;markdown&lt;/code&gt; starts with YAML front matter, so it can go straight into a file-based pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;htmx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;~&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Real&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wasm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;htmx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Port"&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://htmx.org/essays/a-real-world-wasm-to-htmx-port/"&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://htmx.org/essays/a-real-world-wasm-to-htmx-port/"&lt;/span&gt;
&lt;span class="na"&gt;lastmod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-01-10"&lt;/span&gt;
&lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en"&lt;/span&gt;
&lt;span class="na"&gt;word_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;617&lt;/span&gt;
&lt;span class="na"&gt;thin_content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# A Real World wasm to htmx Port&lt;/span&gt;

Joe Fioti

January 10, 2025

When I was in college, I wrote some customer service software that tied together ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the record (trimmed):&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://htmx.org/essays/a-real-world-wasm-to-htmx-port/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonicalUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://htmx.org/essays/a-real-world-wasm-to-htmx-port/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastmod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changeStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contentHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"958fdb17b6246b5bea12b422b88cc12d12204a467949cfee7522513928b8917b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wordCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;617&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokenEstimate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;897&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunkCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunks"&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;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"heading"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A Real World wasm to htmx Port"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# A Real World wasm to htmx Port&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Joe Fioti ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tokenEstimate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;324&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few fields that matter for RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;chunks&lt;/code&gt;&lt;/strong&gt;: each chunk keeps its heading, so you can store it as metadata and cite sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;contentHash&lt;/code&gt;&lt;/strong&gt;: a SHA-256 of the Markdown, handy for "only re-embed what changed".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;canonicalUrl&lt;/code&gt;&lt;/strong&gt;: lets you skip duplicate paths. The Actor already skips exact duplicates by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;thin_content&lt;/code&gt;&lt;/strong&gt;: pages with almost no main content (under 50 words by default) are flagged as &lt;code&gt;thin_page&lt;/code&gt; instead of being written like a normal page. That includes JS-only pages, since there's no browser rendering. They're also left out of &lt;code&gt;llms.txt&lt;/code&gt;, so they don't end up embedded as noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The run also saves &lt;code&gt;llms.txt&lt;/code&gt; (a title + link + description index) and &lt;code&gt;llms-full.txt&lt;/code&gt; (every page's Markdown in one file) to the key-value store. The second one is handy for uploading a whole site to ChatGPT, Claude Projects or NotebookLM. Here's the start of the generated &lt;code&gt;llms.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# htmx.org&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Pages from https://htmx.org/essays, converted to Markdown by the Sitemap to Markdown Actor on 2026-10-18.&lt;/span&gt;

&lt;span class="gu"&gt;## Pages&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;&amp;lt;/&amp;gt; htmx ~ Architectural Sympathy&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://htmx.org/essays/architectural-sympathy/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: In this essay, Carson Gross explores architectural sympathy as a software design principle ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Incremental sync: the part I actually care about
&lt;/h2&gt;

&lt;p&gt;Re-scraping a whole docs site every night to catch two edited pages is wasteful. With &lt;code&gt;incremental: true&lt;/code&gt;, the Actor keeps a small state record in a named key-value store in your own Apify account. On each run it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;skips pages whose sitemap &lt;code&gt;lastmod&lt;/code&gt; hasn't changed (they aren't even downloaded)&lt;/li&gt;
&lt;li&gt;downloads the rest, compares content hashes, and only outputs pages that are &lt;strong&gt;new&lt;/strong&gt; or &lt;strong&gt;changed&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;writes a &lt;code&gt;CHANGES&lt;/code&gt; record listing new, changed, unchanged and removed URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what happened on the last two runs of this input. Earlier runs, with a page cap, had already synced 41 of the 56 essay pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Pages output&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;CHANGES&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catch-up run&lt;/td&gt;
&lt;td&gt;the 15 remaining pages, all &lt;code&gt;new&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;7.1 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new: 15, unchanged: 41&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-run right after&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.6 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new: 0, changed: 0, unchanged: 56, removed: 0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;CHANGES&lt;/code&gt; record is what you wire into your vector store: upsert &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;changed&lt;/code&gt;, delete &lt;code&gt;removed&lt;/code&gt;. Then a nightly schedule only does real work on days the site actually changed.&lt;/p&gt;

&lt;p&gt;Two honest caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;removed&lt;/code&gt; is only reliable when the run reads the whole sitemap: &lt;code&gt;maxPages: 0&lt;/code&gt; and no &lt;code&gt;lastmodAfter&lt;/code&gt; date filter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxPages&lt;/code&gt; limits new and changed pages per run, so with a cap, a big site fills in over several runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;It's pay-per-result on Apify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.20 per 1,000 pages&lt;/strong&gt; saved (that's $0.0002 a page), plus a $0.00005 start fee per run&lt;/li&gt;
&lt;li&gt;Chunks live inside the page record, so chunking costs nothing extra; &lt;code&gt;llms.txt&lt;/code&gt; is free too&lt;/li&gt;
&lt;li&gt;A re-run where nothing changed outputs nothing, so you pay only the start fee&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full htmx essays section (56 pages) costs under 3 cents the first time and about nothing on a quiet night. You can set a max cost per run in Apify, and the Actor stops cleanly at that limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;p&gt;It's plain HTTP, so it won't render JavaScript-only SPAs, log in, or get past aggressive bot protection. Pages from those sites come back flagged as thin, or as a &lt;code&gt;BLOCKED&lt;/code&gt; error instead of silently empty. For those, a browser-based crawler like Apify's Website Content Crawler is the better (more expensive) tool. This one is aimed at docs, blogs, help centers and marketing sites, which is most of what people actually want in a RAG index.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Store page: &lt;a href="https://apify.com/chimey_forearm/sitemap-to-markdown" rel="noopener noreferrer"&gt;Sitemap to Markdown&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;It runs from the Apify console, the API, or as a tool for AI agents through the Apify MCP server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's new, so I'd genuinely like feedback. If the Markdown isn't clean enough for your pipeline, or a field is missing, tell me in the comments or open an issue on the Actor page.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>webscraping</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Validate an IBAN and VAT number before paying an invoice</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Sun, 18 Oct 2026 16:17:04 +0000</pubDate>
      <link>https://dev.to/mowa/validate-an-iban-and-vat-number-before-paying-an-invoice-1cpn</link>
      <guid>https://dev.to/mowa/validate-an-iban-and-vat-number-before-paying-an-invoice-1cpn</guid>
      <description>&lt;p&gt;A mistyped IBAN or a made-up VAT number on a supplier invoice is cheap to catch before payment and expensive to fix after. Here's a small pre-payment check you can add to any accounts-payable flow with two HTTP calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just want to check one number?&lt;/strong&gt; Use the &lt;a href="https://invoice-validator.mohamed-bahaa53000.workers.dev/tools/iban-vat-checker" rel="noopener noreferrer"&gt;free IBAN &amp;amp; VAT checker&lt;/a&gt; in your browser, no sign-up.&lt;/p&gt;

&lt;p&gt;I'll use the Invoice &amp;amp; Payment ID Validator API on RapidAPI (I built it; there's a free plan with 100 requests a month): &lt;a href="https://rapidapi.com/MohamedBahaa/api/invoice-payment-id-validator" rel="noopener noreferrer"&gt;https://rapidapi.com/MohamedBahaa/api/invoice-payment-id-validator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Check the IBAN
&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;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://invoice-payment-id-validator.p.rapidapi.com/v1/iban?value=DE89370400440532013000'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-host: invoice-payment-id-validator.p.rapidapi.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-key: YOUR_RAPIDAPI_KEY'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real response:&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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check_digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"89"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bban"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country_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;"Germany"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bank_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"37040044"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"branch_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89 3704 0044 0532 0130 00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sepa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it checks: the country's IBAN length, the ISO 13616 mod-97 checksum, and SEPA membership. It also splits out the bank code, branch code, and account number for 20 common countries. Change one digit and you get &lt;code&gt;"valid": false&lt;/code&gt; with &lt;code&gt;"errors": ["bad_checksum"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What it doesn't check: whether the account is open or who owns it. A valid checksum means the IBAN is well-formed, not that it belongs to your supplier. For that you still need the supplier's confirmation, or your bank's payee-verification service where one exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check the EU VAT number, live against VIES
&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;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://invoice-payment-id-validator.p.rapidapi.com/v1/vat/eu?value=IE6388047V&amp;amp;live=true'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-host: invoice-payment-id-validator.p.rapidapi.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-key: YOUR_RAPIDAPI_KEY'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real response (request_date trimmed):&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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IE6388047V"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6388047V"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IE6388047V"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format_valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checksum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_checked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&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;"vies"&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;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"GOOGLE IRELAND LIMITED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;live=true&lt;/code&gt;, the API asks the EU's VIES service whether the number is registered. When the member state shares it, you also get the registered name and address, which you can compare with the invoice. Without &lt;code&gt;live=true&lt;/code&gt;, it only checks the format (fast, offline). VIES is sometimes down for individual countries. When it is, &lt;code&gt;vies.error&lt;/code&gt; tells you, so treat that as "retry later", not "invalid".&lt;/p&gt;

&lt;p&gt;UK VAT (&lt;code&gt;/v1/vat/gb&lt;/code&gt;), US EIN (&lt;code&gt;/v1/ein&lt;/code&gt;) and BIC/SWIFT (&lt;code&gt;/v1/bic&lt;/code&gt;) work the same way with &lt;code&gt;?value=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check a whole invoice in one call
&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;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://invoice-payment-id-validator.p.rapidapi.com/v1/batch'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&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;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-host: invoice-payment-id-validator.p.rapidapi.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-rapidapi-key: YOUR_RAPIDAPI_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"items":[{"type":"iban","value":"DE89370400440532013000"},{"type":"eu_vat","value":"IE6388047V","live":true}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Up to 50 IDs per call (up to 10 of them live VIES checks).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A simple pay / hold rule
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hold if &lt;code&gt;iban.valid&lt;/code&gt; is false&lt;/li&gt;
&lt;li&gt;Hold if &lt;code&gt;vies.registered&lt;/code&gt; is false&lt;/li&gt;
&lt;li&gt;Flag for review if the VIES name doesn't roughly match the supplier name on the invoice&lt;/li&gt;
&lt;li&gt;Retry later if &lt;code&gt;vies.error&lt;/code&gt; is set&lt;/li&gt;
&lt;li&gt;Otherwise, pass it to approval&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Do it in n8n
&lt;/h2&gt;

&lt;p&gt;If your invoices arrive by email, my free n8n template turns vendor invoice PDFs in Gmail into clean rows in Google Sheets: &lt;a href="https://mohabaha0.gumroad.com/l/ap-invoice-triage-free" rel="noopener noreferrer"&gt;https://mohabaha0.gumroad.com/l/ap-invoice-triage-free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To add the checks above, put an HTTP Request node after the extraction step: use POST &lt;code&gt;/v1/batch&lt;/code&gt; with the JSON body from step 3 and your two &lt;code&gt;x-rapidapi-*&lt;/code&gt; headers. Then add an IF node with the pay / hold rule.&lt;/p&gt;

&lt;p&gt;If you use AI agents, the API is also an MCP server at &lt;code&gt;/mcp&lt;/code&gt;, so Claude or Cursor can call the validators as tools.&lt;/p&gt;

&lt;p&gt;Questions or edge cases (odd country formats, VIES quirks)? Drop them in the comments.&lt;/p&gt;

</description>
      <category>api</category>
      <category>fintech</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Extracting invoices from Gmail with n8n and Gemini, then checking the IBAN before anyone pays it</title>
      <dc:creator>Mohamed Zaghloul</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:41:33 +0000</pubDate>
      <link>https://dev.to/mowa/extracting-invoices-from-gmail-with-n8n-and-gemini-then-checking-the-iban-before-anyone-pays-it-27fe</link>
      <guid>https://dev.to/mowa/extracting-invoices-from-gmail-with-n8n-and-gemini-then-checking-the-iban-before-anyone-pays-it-27fe</guid>
      <description>&lt;p&gt;Most invoice automations stop at extraction: an LLM pulls the vendor, total and bank details out of a PDF, and the numbers go straight into a spreadsheet or ledger. The risky part is that an LLM can read a perfectly formatted IBAN with one wrong digit, and nothing downstream notices.&lt;/p&gt;

&lt;p&gt;This post walks through a free n8n template that handles the extraction, and a small API I built that handles the checksum part. They're separate pieces: the template doesn't call the API out of the box. I'll show how to add it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: the free n8n template
&lt;/h2&gt;

&lt;p&gt;Nodes, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gmail Trigger: polls for unread mail labelled &lt;code&gt;invoices&lt;/code&gt; with a PDF attached.&lt;/li&gt;
&lt;li&gt;Split PDF Attachments (Code node): one item per PDF, so an email with three invoices produces three rows.&lt;/li&gt;
&lt;li&gt;Extract From File: reads the PDF's text layer. Scanned image-only PDFs need OCR first.&lt;/li&gt;
&lt;li&gt;Gemini + Structured Output Parser: returns vendor, invoice number, PO number, dates, currency, subtotal, tax, total, line items and a confidence score as strict JSON. Temperature 0, retries on rate limits.&lt;/li&gt;
&lt;li&gt;Normalize Invoice (Code node): cleans numbers and dates, checks that subtotal + tax = total, and marks low-confidence rows &lt;code&gt;needs_review&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Google Sheets: appends the row. Then Gmail marks the email read so it's never processed twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Setup takes about 10 minutes: a Gmail label and filter, a sheet with an &lt;code&gt;Invoices&lt;/code&gt; tab, and three credentials (Gmail, Sheets, a free Gemini key from Google AI Studio).&lt;/p&gt;

&lt;p&gt;Two things that made it more reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strict schema over free text. The Structured Output Parser rejects anything that isn't the expected JSON, so a malformed response fails loudly instead of writing garbage.&lt;/li&gt;
&lt;li&gt;Check the math in code, not in the prompt. Asking Gemini "does this add up?" is slower and less reliable than one line of arithmetic in the Normalize node.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 2: validating payment IDs with an API
&lt;/h2&gt;

&lt;p&gt;Extraction confidence doesn't tell you whether an IBAN is real. The ISO 13616 mod-97 checksum does. I put that, plus a few related checks, in a Cloudflare Worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IBAN: country, length, mod-97 checksum, SEPA membership&lt;/li&gt;
&lt;li&gt;BIC: format and country&lt;/li&gt;
&lt;li&gt;EU VAT: format, plus an optional live lookup against the EU's VIES service&lt;/li&gt;
&lt;li&gt;UK VAT: format and checksum&lt;/li&gt;
&lt;li&gt;US EIN: format and IRS prefix (EINs have no check digit, so that's as far as honest validation goes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Up to 50 identifiers per batch request. Here's a real response from the keyless demo 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="s2"&gt;"https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/v1/iban?value=DE89370400440532013000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check_digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"89"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bban"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89 3704 0044 0532 0130 00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sepa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo allows 10 calls per IP per day without a key.&lt;/p&gt;

&lt;p&gt;Why a Worker rather than an n8n Code node? Mostly reuse: the same check is useful from Make, Zapier or a backend, and it's stateless, so it runs at the edge in milliseconds. It also isolates the slow part. VIES throttles by member state, so when it's unavailable the API returns &lt;code&gt;checked: false&lt;/code&gt; with a reason instead of a false "invalid".&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: wiring them together yourself
&lt;/h2&gt;

&lt;p&gt;After Normalize Invoice, add an HTTP Request node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method: POST, to the &lt;code&gt;/v1/batch&lt;/code&gt; endpoint from the RapidAPI listing&lt;/li&gt;
&lt;li&gt;Headers: &lt;code&gt;X-RapidAPI-Key&lt;/code&gt; and &lt;code&gt;X-RapidAPI-Host&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Body:
&lt;/li&gt;
&lt;/ul&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;"items"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-1042-iban"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89370400440532013000"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-1042-vat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu_vat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"live"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In n8n, map each value with an expression from the previous node, for example $json.invoice_number, $json.vendor_iban and $json.vendor_vat.&lt;/p&gt;

&lt;p&gt;You'll need to add &lt;code&gt;vendor_iban&lt;/code&gt; and &lt;code&gt;vendor_vat&lt;/code&gt; to the Gemini schema first. Then treat &lt;code&gt;valid: false&lt;/code&gt; as "needs review", and &lt;code&gt;checked: false&lt;/code&gt; from VIES as "verify manually", not as a rejection. A person sees exactly why a row was flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fail open to a human, never closed to the ledger. Anything that can time out should route to review.&lt;/li&gt;
&lt;li&gt;Batch external checks. One call per identifier multiplies latency and quota.&lt;/li&gt;
&lt;li&gt;Keep each piece honest about what it does. The template extracts; the API validates; you decide how strict to be.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free n8n template: &lt;a href="https://mohabaha0.gumroad.com/l/ap-invoice-triage-free" rel="noopener noreferrer"&gt;https://mohabaha0.gumroad.com/l/ap-invoice-triage-free&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Invoice &amp;amp; Payment ID Validator on RapidAPI: &lt;a href="https://rapidapi.com/MohamedBahaa/api/invoice-payment-id-validator" rel="noopener noreferrer"&gt;https://rapidapi.com/MohamedBahaa/api/invoice-payment-id-validator&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback on either is welcome, especially edge cases where the extraction or the validation gets it wrong.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>ai</category>
      <category>api</category>
    </item>
  </channel>
</rss>
