<?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: Russell Yapp</title>
    <description>The latest articles on DEV Community by Russell Yapp (@yappman).</description>
    <link>https://dev.to/yappman</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%2F4141609%2Fb993867c-5235-488f-b905-284f38e7a57b.png</url>
      <title>DEV Community: Russell Yapp</title>
      <link>https://dev.to/yappman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yappman"/>
    <language>en</language>
    <item>
      <title>Tracking changes to the UK sponsor register with a change feed</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:57:02 +0000</pubDate>
      <link>https://dev.to/yappman/tracking-changes-to-the-uk-sponsor-register-with-a-change-feed-ml7</link>
      <guid>https://dev.to/yappman/tracking-changes-to-the-uk-sponsor-register-with-a-change-feed-ml7</guid>
      <description>&lt;p&gt;The UK Home Office publishes a register of licensed sponsors for the Worker and Temporary Worker routes. It publishes only the latest file. If an organisation was added last month, or dropped off last week, the register does not tell you when.&lt;/p&gt;

&lt;p&gt;I have built an Apify Actor, UK Sponsor Register Changes, that keeps that history from the day we started collecting. It has been public since 3 November 2026. This post explains what it returns, how to run it, and what it will not tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a change feed rather than a full list
&lt;/h2&gt;

&lt;p&gt;The register is useful as a lookup. It is less useful if your question is "what changed since I last looked?". Compliance, HR and immigration advisers, and analysts who follow the sector, usually want that second question answered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which organisations have been added;&lt;/li&gt;
&lt;li&gt;which are no longer listed;&lt;/li&gt;
&lt;li&gt;which have had a rating changed;&lt;/li&gt;
&lt;li&gt;which have had a route added or removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comparing two large files by hand each time is slow and easy to get wrong. A feed gives you only the rows that differ, each with a date, so you pay for and read only the changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a change row contains
&lt;/h2&gt;

&lt;p&gt;One row per change. According to the README, each row has the date, the organisation, town, county, route, the old and new type and rating, where it came from, and its id and sequence. The README's sample row (an invented organisation, not a real one):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;changeType&lt;/th&gt;
&lt;th&gt;organisation&lt;/th&gt;
&lt;th&gt;town&lt;/th&gt;
&lt;th&gt;county&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;oldTypeRating&lt;/th&gt;
&lt;th&gt;newTypeRating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-01-05&lt;/td&gt;
&lt;td&gt;added&lt;/td&gt;
&lt;td&gt;Example Org Ltd&lt;/td&gt;
&lt;td&gt;Exampleton&lt;/td&gt;
&lt;td&gt;Examplesire&lt;/td&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;Worker (A rating)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The change types are an organisation added, no longer listed, a rating changed, or a route added or removed. "Removed" means only that the organisation is not in the file of that date. It does not say why.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;

&lt;p&gt;The Store page is here: &lt;a href="https://apify.com/yappman/uk-sponsor-register-changes" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-sponsor-register-changes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The simplest input caps the run at three rows:&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;"maxItems"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Console form starts at 3 rows, so a default run costs up to $0.015. To narrow a run, the README's example input uses a start date, a change type and a county:&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;"changedSince"&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-10-19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"changeTypes"&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;"added"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"counties"&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;"West Yorkshire"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&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;Set &lt;code&gt;maxItems&lt;/code&gt; to cap what a run can cost. A call that leaves it out returns up to 100 rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it will not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not advice.&lt;/strong&gt; It is a dated record of changes to a public register. Being on the register says nothing about the status of any job or any visa outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a compliance check on a named employer.&lt;/strong&gt; I do not offer it as a way to confirm an employer's current standing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not have the register's full history.&lt;/strong&gt; The register publishes only its latest file, so history starts on the day we began collecting. Collection is intended each working day, and any day we missed is listed in &lt;code&gt;RUN_SUMMARY&lt;/code&gt; (&lt;code&gt;daysMissed&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not give names of individuals, by design.&lt;/strong&gt; Entries that look like an individual's name are designed to be left out and counted in &lt;code&gt;RUN_SUMMARY&lt;/code&gt; (&lt;code&gt;withheldByType&lt;/code&gt;; counts under 5 show as &lt;code&gt;&amp;lt;5&lt;/code&gt;). The check is rule-based and can be wrong both ways. It can leave out a genuine organisation, and it can let through a company, school or trading name that belongs to one person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It holds back unusual days.&lt;/strong&gt; If a comparison shows more than 3% of the register changing in one day, we hold it back for checking rather than publish it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your use is your responsibility.&lt;/strong&gt; We cannot control what happens to rows once they are delivered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Pay per event: $0.005 for each change row returned, which is $5.00 per 1,000. The price is the same on every plan, with no start fee and no run fee. A run that is refused before any data is read, that finds no changes in your window, or that finds the change log unavailable costs nothing.&lt;/p&gt;

&lt;p&gt;Some figures from the README: a run capped at 3 rows costs up to $0.015; a run with no cap returns up to 100 rows, so up to $0.50. For comparison with other tools, check other sponsor-register Actors on the Store yourself; I make no claim about relative price.&lt;/p&gt;

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

&lt;p&gt;The rows are organisation names from a public register, and some organisations are sole traders. The privacy notice for this feed, including how to object or ask for an entry to be removed, is at &lt;a href="https://data.yappman.com/privacy/" rel="noopener noreferrer"&gt;https://data.yappman.com/privacy/&lt;/a&gt;. A removal changes our record and later runs. It cannot recall rows already delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licence and attribution
&lt;/h2&gt;

&lt;p&gt;Contains public sector information licensed under the Open Government Licence v3.0. Source: Register of licensed sponsors: workers, UK Visas and Immigration, GOV.UK.&lt;/p&gt;

&lt;p&gt;Not affiliated with or endorsed by the Home Office or UK Visas and Immigration.&lt;/p&gt;

&lt;p&gt;I would like to hear if a field is missing or a row looks wrong. Ratings and reviews on the Store page also help other buyers judge whether it fits.&lt;/p&gt;

&lt;p&gt;Russell Yapp, Yappman Data&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>uk</category>
      <category>apify</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Week 44 shelf-price indices: UK down 0.24%, Australia up 0.48%</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Mon, 02 Nov 2026 11:56:23 +0000</pubDate>
      <link>https://dev.to/yappman/week-44-shelf-price-indices-uk-down-024-australia-up-048-17l8</link>
      <guid>https://dev.to/yappman/week-44-shelf-price-indices-uk-down-024-australia-up-048-17l8</guid>
      <description>&lt;p&gt;I publish two weekly indices of posted online supermarket prices, one for the UK and one for Australia. Both for 2026-W44 went out on 2 November 2026. This post covers the headline moves, what the indices do not measure, and how to read the data yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the indices are
&lt;/h2&gt;

&lt;p&gt;Each week I read the online catalogues of the retailers, match products week on week by the retailer's own product id, and chain the geometric mean (a Jevons index) of the price ratios. Retailers get equal weights. There are no spending weights. The indices are intended weekly, normally on Monday.&lt;/p&gt;

&lt;p&gt;They are not CPI and not an official statistic. They measure posted online shelf prices, not prices paid.&lt;/p&gt;

&lt;h2&gt;
  
  
  UK, 2026-W44
&lt;/h2&gt;

&lt;p&gt;The headline for Tesco, Sainsbury's, Asda and Aldi is 99.72, down 0.24% on the week. The base is 100 at 2026-W42, when the series restarted after a skipped week 41. It rests on 74,335 matched products. Food and soft drinks is 99.81, down 0.21%.&lt;/p&gt;

&lt;p&gt;By retailer, week on week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tesco: +0.05%&lt;/li&gt;
&lt;li&gt;Sainsbury's: +0.04%&lt;/li&gt;
&lt;li&gt;Asda: -0.17%&lt;/li&gt;
&lt;li&gt;Aldi: -0.88%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not giving a cause for Aldi's fall. The index shows the movement only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Australia, 2026-W44
&lt;/h2&gt;

&lt;p&gt;The like-for-like headline for Coles, Woolworths and Aldi Australia is 100.64, up 0.48%, from a base of 100 at 2026-W41. It rests on 21,029 matched products, in Australian dollars. Food and soft drinks is 100.23, up 0.05%.&lt;/p&gt;

&lt;p&gt;Across all matched products, by retailer: Coles +0.15%, Woolworths +1.19%, Aldi Australia -0.10%.&lt;/p&gt;

&lt;p&gt;Personal care is the class that moved most, up 7.15% on the page. Shelf prices include promotions by design, so a change in how many products are on offer can move a class even when regular prices do not.&lt;/p&gt;

&lt;p&gt;One limit matters here: Coles is a partial slice, with 6,425 products used (21.0% of the Coles lists) this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the data
&lt;/h2&gt;

&lt;p&gt;The full UK series is a public CSV under CC BY 4.0. It is long format, with one row per vintage, record type, retailer and class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/datasets/AvRCWqFinCiNjx3sP/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?format=csv&amp;amp;clean=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vintage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekOnWeekPct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matched&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vintage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column names come from the data dictionary. Levels from different series (base W39 and base W42) cannot be compared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to get it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UK W44: DOI &lt;a href="https://doi.org/10.5281/zenodo.23881940" rel="noopener noreferrer"&gt;10.5281/zenodo.23881940&lt;/a&gt;, and the Hugging Face dataset &lt;a href="https://huggingface.co/datasets/yappman/uk-online-shelf-price-index" rel="noopener noreferrer"&gt;yappman/uk-online-shelf-price-index&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;AU W44: DOI &lt;a href="https://doi.org/10.5281/zenodo.23882013" rel="noopener noreferrer"&gt;10.5281/zenodo.23882013&lt;/a&gt;, and the Hugging Face dataset &lt;a href="https://huggingface.co/datasets/yappman/au-online-shelf-price-index" rel="noopener noreferrer"&gt;yappman/au-online-shelf-price-index&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Pages and methods: &lt;a href="https://data.yappman.com/shelf-price-index/" rel="noopener noreferrer"&gt;UK index&lt;/a&gt;, &lt;a href="https://data.yappman.com/au-shelf-price-index/" rel="noopener noreferrer"&gt;AU index&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The indices publish aggregates only. Product-level shelf prices can be read on demand with the &lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;UK Supermarket Price Scraper&lt;/a&gt;, a pay-per-event Actor on the Apify Store. For Australia, the &lt;a href="https://apify.com/yappman/au-supermarket-price-scraper" rel="noopener noreferrer"&gt;Australia Supermarket Price Scraper&lt;/a&gt; reads Aldi Australia; the AU index page says Coles and Woolworths are read only on runs started by the Actor owner's account. Check each Store page for current pricing.&lt;/p&gt;

&lt;p&gt;Cite as "Yappman Data, UK Online Shelf-Price Index" or "Yappman Data, AU Online Shelf-Price Index", CC BY 4.0, with the vintage.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>python</category>
      <category>datascience</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Australian Government contract notices from AusTender as fixed rows, with supplier names left out</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Sun, 01 Nov 2026 10:43:41 +0000</pubDate>
      <link>https://dev.to/yappman/australian-government-contract-notices-from-austender-as-fixed-rows-with-supplier-names-left-out-2f7c</link>
      <guid>https://dev.to/yappman/australian-government-contract-notices-from-austender-as-fixed-rows-with-supplier-names-left-out-2f7c</guid>
      <description>&lt;p&gt;Australian Government agencies publish the contracts they award on AusTender, the government's procurement site, which the Department of Finance runs. Every contract at or above the reporting threshold appears there as a contract notice, and notices can later be amended. The data is open, and AusTender has an API that returns it in the Open Contracting (OCDS) format.&lt;/p&gt;

&lt;p&gt;I built an Apify Actor that reads those contract notices and returns one fixed row per notice: &lt;a href="https://apify.com/yappman/au-contract-notices" rel="noopener noreferrer"&gt;AusTender Contract Notices&lt;/a&gt;. You can look up notices by id, search by date and filters, or run a daily alert that returns only notices it has not sent before.&lt;/p&gt;

&lt;p&gt;It is independent: not affiliated with or endorsed by the Department of Finance or AusTender.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the data is
&lt;/h2&gt;

&lt;p&gt;AusTender materials are released by the Department of Finance under the Creative Commons Attribution 3.0 Australia licence (CC BY 3.0 AU). Only contracts at or above the reporting threshold are published on AusTender: A$10,000 for most agencies, higher for corporate Commonwealth entities (Commonwealth Procurement Rules, paragraph 7.18). These are contract awards and their amendments. They are not open tenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a row holds
&lt;/h2&gt;

&lt;p&gt;One row per notice or amendment, in a fixed field order. This is the README's sample, shortened to the main fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;cnId&lt;/th&gt;
&lt;th&gt;agency&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;th&gt;currency&lt;/th&gt;
&lt;th&gt;contractStart&lt;/th&gt;
&lt;th&gt;contractEnd&lt;/th&gt;
&lt;th&gt;publishedAt&lt;/th&gt;
&lt;th&gt;procurementMethod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CN4280221&lt;/td&gt;
&lt;td&gt;National Indigenous Australians Agency&lt;/td&gt;
&lt;td&gt;44490.6&lt;/td&gt;
&lt;td&gt;AUD&lt;/td&gt;
&lt;td&gt;2026-09-22T14:00:00Z&lt;/td&gt;
&lt;td&gt;2027-06-29T14:00:00Z&lt;/td&gt;
&lt;td&gt;2026-10-01T23:51:13Z&lt;/td&gt;
&lt;td&gt;Limited tender&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full row also has these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;parentCnId&lt;/code&gt; (the parent notice for an amendment, otherwise null), &lt;code&gt;ocid&lt;/code&gt; and &lt;code&gt;releaseId&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt;, the agency's contract reference as published, and &lt;code&gt;unspscCodes&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;limitedTenderExempt&lt;/code&gt;, the notice's exemption flag as published;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dateSigned&lt;/code&gt;, &lt;code&gt;lastModifiedAt&lt;/code&gt; and &lt;code&gt;amendments&lt;/code&gt; (the number of amendments);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noticeUrl&lt;/code&gt;, which opens the notice on the AusTender API;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alertFingerprint&lt;/code&gt; (alert mode only), plus &lt;code&gt;attribution&lt;/code&gt; and &lt;code&gt;licence&lt;/code&gt;, which carry the credit line on every row.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is left out
&lt;/h2&gt;

&lt;p&gt;Version 1 outputs no supplier's name, legal name, ABN or party id, and has no supplier filter. The notice link (&lt;code&gt;noticeUrl&lt;/code&gt;) opens the notice on the AusTender API, which does name the supplier.&lt;/p&gt;

&lt;p&gt;A notice's description (its subject line) can name a person, so it is never output and there is no keyword search. The &lt;code&gt;title&lt;/code&gt; field is the agency's contract reference, not a subject. It is output only when it is reference-shaped: it contains a digit, contains no whitespace and is at most 40 characters. Otherwise it is null. Contact details and addresses are never read.&lt;/p&gt;

&lt;p&gt;If a notice names you, or your business as a sole trader, the README says you can ask for it to be left out by emailing &lt;a href="mailto:info@yappman.com"&gt;info@yappman.com&lt;/a&gt; with the CN id; the Actor acts on a request within one month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three modes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lookup&lt;/strong&gt; (&lt;code&gt;cnIds&lt;/code&gt;): up to 20 CN ids, such as &lt;code&gt;CN4280221&lt;/code&gt;, or an amendment id such as &lt;code&gt;CN4271813-A1&lt;/code&gt;. Each returns the latest version of its notice, with its amendment count. An id AusTender does not know is reported as not found and costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt;: a date range of up to 92 days (default 31) on one of four dates (&lt;code&gt;dateType&lt;/code&gt;: &lt;code&gt;contractPublished&lt;/code&gt;, &lt;code&gt;contractLastModified&lt;/code&gt;, &lt;code&gt;contractStart&lt;/code&gt; or &lt;code&gt;contractEnd&lt;/code&gt;). Filters are &lt;code&gt;agencies&lt;/code&gt;, &lt;code&gt;unspscCodes&lt;/code&gt;, &lt;code&gt;valueMin&lt;/code&gt; and &lt;code&gt;valueMax&lt;/code&gt; (in AUD), &lt;code&gt;procurementMethods&lt;/code&gt; (&lt;code&gt;Open tender&lt;/code&gt;, &lt;code&gt;Limited tender&lt;/code&gt;, &lt;code&gt;Prequalified tender&lt;/code&gt;), and contract start and end dates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert&lt;/strong&gt; (&lt;code&gt;alertId&lt;/code&gt;): run it daily. Each run reads the last 14 days of contract last-modified dates, so amendments are caught, and returns only the notices this alert has never sent before. An alert keeps what it has sent in your own key-value store, named after the alert. Do not put a person's name in &lt;code&gt;alertId&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An alert sends at most &lt;code&gt;maxItems&lt;/code&gt; new notices a run, newest first, so narrow it with filters and set &lt;code&gt;maxItems&lt;/code&gt; for its expected volume. The README says unfiltered daily volume is in the high hundreds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example input
&lt;/h2&gt;

&lt;p&gt;A search for one agency's larger contracts published in October 2026:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contractPublished"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateFrom"&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-10-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateTo"&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-10-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agencies"&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;"National Indigenous Australians Agency"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valueMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"procurementMethods"&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;"Limited tender"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&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;And from Python, with &lt;code&gt;apify-client&lt;/code&gt; and an Apify token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/au-contract-notices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cnIds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CN4280221&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;max_total_charge_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cnId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;procurementMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Pay per event: one &lt;code&gt;notice&lt;/code&gt; event for each row returned, in every mode. On the Apify Free plan that is $0.002 per notice, which is $2.00 per 1,000. It is $0.0018 on Bronze, $0.0016 on Silver and $0.0012 on Gold and above. There is no start or run fee, so a run that returns no notices costs nothing.&lt;/p&gt;

&lt;p&gt;Worked examples on the Free plan, from the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Console default run (the form starts &lt;code&gt;maxItems&lt;/code&gt; at 3) costs up to $0.006;&lt;/li&gt;
&lt;li&gt;one notice looked up costs $0.002;&lt;/li&gt;
&lt;li&gt;through the API, &lt;code&gt;maxItems&lt;/code&gt; defaults to 1,000, so a run with no &lt;code&gt;maxItems&lt;/code&gt; costs up to $2.00. Set it;&lt;/li&gt;
&lt;li&gt;an unfiltered alert with &lt;code&gt;maxItems&lt;/code&gt; 1000 costs up to $2.00 a day.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This is not legal or procurement advice, and the Actor does not claim to be complete. Only contracts at or above the reporting threshold are on AusTender at all. AusTender materials are subject to change and should be verified on the AusTender website (&lt;a href="http://www.tenders.gov.au" rel="noopener noreferrer"&gt;www.tenders.gov.au&lt;/a&gt;). Because supplier names are left out and there is no keyword search, it will not tell you who won a contract; the notice link on AusTender does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source and credit
&lt;/h2&gt;

&lt;p&gt;The output carries this credit on every row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AusTender materials are released by the Department of Finance. AusTender materials are subject to change and should be verified on the AusTender website to ensure the information is up to date and correct: &lt;a href="http://www.tenders.gov.au" rel="noopener noreferrer"&gt;www.tenders.gov.au&lt;/a&gt;. Derived and modified by Yappman Data from AusTender contract notices: fields selected, renamed and redacted, and supplier names and identifiers left out.&lt;/li&gt;
&lt;li&gt;Licensed under CC BY 3.0 AU: &lt;a href="https://creativecommons.org/licenses/by/3.0/au/" rel="noopener noreferrer"&gt;https://creativecommons.org/licenses/by/3.0/au/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Store page has the full input reference: &lt;a href="https://apify.com/yappman/au-contract-notices" rel="noopener noreferrer"&gt;yappman/au-contract-notices&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>apify</category>
      <category>opendata</category>
      <category>australia</category>
    </item>
    <item>
      <title>Australian education providers (CRICOS) as clean rows, with the names that look like people left out</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Sat, 31 Oct 2026 12:16:54 +0000</pubDate>
      <link>https://dev.to/yappman/australian-education-providers-cricos-as-clean-rows-with-the-names-that-look-like-people-left-out-4gf3</link>
      <guid>https://dev.to/yappman/australian-education-providers-cricos-as-clean-rows-with-the-names-that-look-like-people-left-out-4gf3</guid>
      <description>&lt;p&gt;Australia keeps a public register of education providers that may enrol overseas students: the Commonwealth Register of Institutions and Courses for Overseas Students, known as CRICOS. The Department of Education publishes it on data.gov.au as CSV files under the Creative Commons Attribution 2.5 Australia licence (CC BY 2.5 AU). The files are open, but they come as separate tables with long address blocks, and a provider can be a company, a university, a TAFE or a sole trader.&lt;/p&gt;

&lt;p&gt;I built an Apify Actor that reads two of the files, Institutions and Courses, and returns one fixed row per provider: &lt;a href="https://apify.com/yappman/cricos-register" rel="noopener noreferrer"&gt;CRICOS Register Lookup (Australia)&lt;/a&gt;. It is ready for a spreadsheet, a script or an AI agent.&lt;/p&gt;

&lt;p&gt;Not affiliated with or endorsed by the Department of Education.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a row holds
&lt;/h2&gt;

&lt;p&gt;One row per provider, in a fixed field order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;providerCode&lt;/code&gt;, the CRICOS provider code (five digits and a capital letter, for example &lt;code&gt;00001K&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;providerName&lt;/code&gt;, when the name rule shows it (see below), and &lt;code&gt;nameWithheld&lt;/code&gt;, which says whether the name is left out;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;institutionType&lt;/code&gt; as the register spells it (for example Government or Private);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state&lt;/code&gt;, &lt;code&gt;postcode&lt;/code&gt; and &lt;code&gt;city&lt;/code&gt; of the provider's postal address;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;institutionCapacity&lt;/code&gt;, as a whole number;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;courseCount&lt;/code&gt; (rows in the Courses file for the provider, expired courses included) and &lt;code&gt;activeCourseCount&lt;/code&gt; (the ones the register marks as not expired);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;website&lt;/code&gt;, when neither name is left out;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snapshotDate&lt;/code&gt;, from the Last-Modified header of the Institutions file the run read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are no postal address lines, location names, contact details or course fees in the output. The Institutions file does contain addresses and websites; the Actor reads it in memory for the run and returns only the fields above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two modes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;search&lt;/strong&gt; (the default) returns the providers that pass your filters, sorted by provider code. Filters are &lt;code&gt;states&lt;/code&gt; (ACT, NSW, NT, QLD, SA, TAS, VIC, WA), &lt;code&gt;institutionTypes&lt;/code&gt;, up to 50 &lt;code&gt;postcodes&lt;/code&gt;, and &lt;code&gt;nameContains&lt;/code&gt; (2 to 80 characters, matched against the name in the output). &lt;code&gt;maxItems&lt;/code&gt; caps the rows returned: 100 by default through the API, 10 in the Console form, at most 2,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lookup&lt;/strong&gt; takes 1 to 20 provider codes. Codes that are not in the register are listed in the run summary and the rest are returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The name rule
&lt;/h2&gt;

&lt;p&gt;Some providers are sole traders, and a register of businesses can still carry a person's name. So the Actor does not simply print every name. It leaves a name out unless the name rule finds an organisation cue and no person shape. Where the name is left out, the row still has the provider code, type, state, postcode and city, and &lt;code&gt;nameWithheld&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule is a pattern test, not a judgement about any person. It can be wrong both ways: it can let through a college named after a person, and it leaves out a name made of what looks like a person's name plus a word such as College, even when it belongs to an organisation. A search by name only matches the names that are shown, so it cannot be used to find a name that was left out.&lt;/p&gt;

&lt;p&gt;The share left out is about 24% of providers (365 of 1,547, measured 28 October 2026). A row whose name is left out still gives the postal state, postcode and city, which for a sole trader can be a home locality. If you appear in the register and want your entry left out, the privacy page on the Yappman Data website describes how to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price
&lt;/h2&gt;

&lt;p&gt;$0.002 per row returned, which is $2.00 per 1,000 rows, flat on every Apify plan. There is no start fee and no run fee. Refused, empty and source-problem runs are not charged. The run's maximum charge stops it at the last row it can pay for.&lt;/p&gt;

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

&lt;p&gt;Do not use it to decide whether a provider may enrol a student, or as proof of a provider's registration. The register is the authority; the Actor returns a dated copy of two of its files. A provider that is missing from a result has not necessarily closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source and credit
&lt;/h2&gt;

&lt;p&gt;The data is CC BY 2.5 AU. The Actor's output carries this line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contains data from the Commonwealth Register of Institutions and Courses for Overseas Students (CRICOS), Department of Education, Australian Government, licensed under Creative Commons Attribution 2.5 Australia (CC BY 2.5 AU). This Actor filters and reshapes the data; changes were made.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It makes two requests to data.gov.au per run, builds them itself (the input takes no URLs, headers or query strings) and stops if the source answers with a rate-limit refusal.&lt;/p&gt;

&lt;p&gt;The Store page is here: &lt;a href="https://apify.com/yappman/cricos-register" rel="noopener noreferrer"&gt;yappman/cricos-register&lt;/a&gt;. Ratings and reviews there help other buyers judge whether this Actor fits their use.&lt;/p&gt;

</description>
      <category>python</category>
      <category>apify</category>
      <category>opendata</category>
      <category>australia</category>
    </item>
    <item>
      <title>Aldi Netherlands, Aldi Nord and Aldi Süd shelf prices in EUR</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:15:35 +0000</pubDate>
      <link>https://dev.to/yappman/aldi-netherlands-aldi-nord-and-aldi-sud-shelf-prices-in-eur-1da9</link>
      <guid>https://dev.to/yappman/aldi-netherlands-aldi-nord-and-aldi-sud-shelf-prices-in-eur-1da9</guid>
      <description>&lt;p&gt;I have published an Apify Actor that reads shelf prices from three Aldi shops: Aldi Netherlands (&lt;code&gt;aldi.nl&lt;/code&gt;), Aldi Nord Germany (&lt;code&gt;aldi-nord.de&lt;/code&gt;) and Aldi Süd Germany (&lt;code&gt;aldi-sued.de&lt;/code&gt;). All three return the same row shape, in euros. This post covers what a row holds, the three ways to run it, what it costs and what it does not do.&lt;/p&gt;

&lt;p&gt;The Actor is &lt;a href="https://apify.com/yappman/nl-de-supermarket-price-scraper" rel="noopener noreferrer"&gt;Aldi Netherlands &amp;amp; Germany Price Scraper: Shelf Prices (EUR)&lt;/a&gt; on the Apify Store. It is an independent tool, not affiliated with, endorsed by or sponsored by ALDI Nederland, ALDI Nord, ALDI SÜD or any ALDI company.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both German Aldi chains
&lt;/h2&gt;

&lt;p&gt;Germany has two Aldi chains, with their own shops and prices. This Actor reads both: Aldi Nord (&lt;code&gt;aldi-nord.de&lt;/code&gt;, &lt;code&gt;retailer&lt;/code&gt; &lt;code&gt;aldi_nord_de&lt;/code&gt;) and Aldi Süd (&lt;code&gt;aldi-sued.de&lt;/code&gt;, &lt;code&gt;retailer&lt;/code&gt; &lt;code&gt;aldi_sued_de&lt;/code&gt;). They are different sites, and each row names the shop it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a row holds
&lt;/h2&gt;

&lt;p&gt;Each row is one product. Here is a sample from &lt;code&gt;aldi.nl&lt;/code&gt; (from a capture of 27 Oct 2026):&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;"retailer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aldi_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;"retailerProductId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1235974"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ean"&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;"Romige kwark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MILSANI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.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;"wasPrice"&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;"unitPrice"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"measure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kg"&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;"deposit"&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;"promotionText"&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;"categoryPath"&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;"Zuivel, eieren en boter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verse zuivel"&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://www.aldi.nl/product/romige-kwark-1235974.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"milsani romige kwark 500g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"market"&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="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 need a word:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unitPrice&lt;/code&gt;&lt;/strong&gt; is the shop's own figure, as the shelf shows it. &lt;code&gt;aldi.nl&lt;/code&gt; gives one on about three quarters of products, &lt;code&gt;aldi-nord.de&lt;/code&gt; gave one on every record I have seen, and &lt;code&gt;aldi-sued.de&lt;/code&gt; on every record I have seen except loose fruit and vegetables. Where a shop shows none, the field is null. The Actor never calculates one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deposit&lt;/code&gt;&lt;/strong&gt; is the bottle deposit (Pfand) in euros, as the shop shows it, for example &lt;code&gt;0.25&lt;/code&gt;. It is filled on &lt;code&gt;aldi-nord.de&lt;/code&gt; and &lt;code&gt;aldi-sued.de&lt;/code&gt; where the shop shows one, and is null otherwise. On &lt;code&gt;aldi.nl&lt;/code&gt; it is always null. &lt;code&gt;price&lt;/code&gt; stays the shelf price as the site gives it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wasPrice&lt;/code&gt; and &lt;code&gt;promotionText&lt;/code&gt;&lt;/strong&gt; are filled while a reduction is shown (for example &lt;code&gt;-10%&lt;/code&gt;). On &lt;code&gt;aldi-nord.de&lt;/code&gt; a reduced price has not yet been seen in a real capture. On &lt;code&gt;aldi-sued.de&lt;/code&gt; one was seen in a capture of 28 Oct 2026; that shop gives no end date for a reduction, so &lt;code&gt;promotionEndsAt&lt;/code&gt; is null there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ean&lt;/code&gt;&lt;/strong&gt; is always empty, because Aldi shows no barcode on these sites. &lt;code&gt;matchKey&lt;/code&gt; is a name and pack-size key you can join on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;categoryPath&lt;/code&gt;&lt;/strong&gt; is in Dutch or German, as each shop names it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not returned:&lt;/strong&gt; images, product descriptions, ingredients, nutrition, reviews and ratings. &lt;code&gt;imageUrl&lt;/code&gt; is always null, and &lt;code&gt;inStock&lt;/code&gt; is null because stock is per store and not shown online.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;What it reads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;category&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;categoryUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every product on a category listing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;product&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;productUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Named product pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalogue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;retailers&lt;/code&gt;, &lt;code&gt;maxItems&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The listing pages a shop's sitemap names (about 135 for &lt;code&gt;aldi.nl&lt;/code&gt;, 120 for &lt;code&gt;aldi-nord.de&lt;/code&gt;); for &lt;code&gt;aldi-sued.de&lt;/code&gt;, the 49 top-level category pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three shops work in all three modes. In category mode, &lt;code&gt;aldi.nl&lt;/code&gt; and &lt;code&gt;aldi-nord.de&lt;/code&gt; need a subcategory page, because their top-level category pages carry no product list and are refused with a message. On &lt;code&gt;aldi-sued.de&lt;/code&gt; a top-level category page lists products too, and a listing is read page by page (30 products a page) until &lt;code&gt;maxItems&lt;/code&gt; or the end of the listing. In catalogue mode &lt;code&gt;retailers&lt;/code&gt; defaults to &lt;code&gt;["aldi_nl"]&lt;/code&gt;; use &lt;code&gt;aldi_nord_de&lt;/code&gt; and &lt;code&gt;aldi_sued_de&lt;/code&gt; for Germany.&lt;/p&gt;

&lt;p&gt;There is no search mode. Category and product modes take the shop from each page address, so you can mix the shops in one run. &lt;code&gt;maxItems&lt;/code&gt; caps the rows saved (default 60).&lt;/p&gt;

&lt;p&gt;The default input reads one small &lt;code&gt;aldi.nl&lt;/code&gt; page (eggs, about 4 products) and finishes in under a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it from Python
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;apify-client&lt;/code&gt; and an Apify token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/nl-de-supermarket-price-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoryUrls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.aldi.nl/producten/zuivel-eieren-boter/verse-zuivel.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.aldi-nord.de/sortiment/milchprodukte/milch-milchgetraenke.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.aldi-sued.de/produkte/kaese/frischkaese/k/1588161425467086&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;max_total_charge_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.055&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unitPrice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packSize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;measure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That run returns at most 30 rows in total. &lt;code&gt;max_total_charge_usd&lt;/code&gt; stops the run if its charges would pass the cap.&lt;/p&gt;

&lt;p&gt;Each run also produces a run summary, which counts the records skipped for having no current price.&lt;/p&gt;

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

&lt;p&gt;The Actor is pay per event. There is a start fee of $0.005 per run, plus one &lt;code&gt;product&lt;/code&gt; event for each row returned, and no other event. On the Apify Free plan a product costs $0.0015, which is $1.50 per 1,000. On higher plans it costs less, down to $0.90 per 1,000.&lt;/p&gt;

&lt;p&gt;Worked examples on the Free plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the default run (about 4 rows) costs $0.005 + 4 x $0.0015 = $0.011;&lt;/li&gt;
&lt;li&gt;the three-shop run above costs at most $0.005 + 30 x $0.0015 = $0.05;&lt;/li&gt;
&lt;li&gt;1,000 rows cost $1.505.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A run that returns nothing costs only the start fee. The Store page has the current prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it reads the shops
&lt;/h2&gt;

&lt;p&gt;At the start of every run the Actor requests each selected shop's robots.txt. Requests go out one at a time, about one every two to three seconds, from the Apify platform's own network. If a shop answers 403 or 429, the run stops for that shop at once, with no retry. A server error is retried a few times, then stops that shop too, and so does any other answer that is not a normal page. You pay only for the rows already returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prices are those shown to a visitor with no store chosen, and they are current at the row's &lt;code&gt;scrapedAt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt; is the shelf price as the site gives it, and &lt;code&gt;deposit&lt;/code&gt; is returned separately. The Actor makes no claim about whether a shelf price includes the deposit.&lt;/li&gt;
&lt;li&gt;Records without a current price, or not on sale yet, are skipped and counted in the run summary. That covers a promotion that has not started and an &lt;code&gt;aldi-sued.de&lt;/code&gt; offer dated later than the run.&lt;/li&gt;
&lt;li&gt;Aldi Nord and Aldi Süd are separate shops with their own prices, so the same product can differ between them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Store page has the full field list, the input reference and the changelog: &lt;a href="https://apify.com/yappman/nl-de-supermarket-price-scraper" rel="noopener noreferrer"&gt;Aldi Netherlands &amp;amp; Germany Price Scraper: Shelf Prices (EUR)&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>apify</category>
      <category>opendata</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Ireland rent and house price index data as clean rows (RTB / CSO)</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Thu, 29 Oct 2026 15:30:25 +0000</pubDate>
      <link>https://dev.to/yappman/ireland-rent-and-house-price-index-data-as-clean-rows-rtb-cso-2f3a</link>
      <guid>https://dev.to/yappman/ireland-rent-and-house-price-index-data-as-clean-rows-rtb-cso-2f3a</guid>
      <description>&lt;p&gt;Ireland publishes two useful housing tables through the Central Statistics Office (CSO): the RTB Average Monthly Rent Report (table RIQ02) and the Residential Property Price Index (table HPM09). Both are open data under CC BY 4.0. Both come as JSON-stat cubes, which are compact but awkward to filter: one long array of values, with dimensions you have to decode before you can ask "what is the average rent for a two-bed apartment in Carlow?".&lt;/p&gt;

&lt;p&gt;I built an Apify Actor that reads those two tables and returns one fixed row per figure, ready for a spreadsheet, a script or an AI agent: &lt;a href="https://apify.com/yappman/ireland-rent-index" rel="noopener noreferrer"&gt;Ireland Rent and House Price Index (RTB / CSO)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Not affiliated with or endorsed by the Residential Tenancies Board (RTB) or the Central Statistics Office (CSO).&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Rent (RIQ02, quarterly).&lt;/strong&gt; Average monthly rent by place, number of bedrooms, property type and quarter. The places run from counties down to towns and local areas. Where the source marks a cell as insufficient data, that cell is empty and the Actor leaves it out (and does not charge for it).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;House price index (HPM09, monthly).&lt;/strong&gt; The CSO's Residential Property Price Index, base 2015 = 100, monthly from January 2005, in 20 categories that combine an area and a type of home: &lt;code&gt;National - houses&lt;/code&gt;, &lt;code&gt;Dublin - apartments&lt;/code&gt;, &lt;code&gt;Dublin City - houses&lt;/code&gt;, the regions and so on. No category is finer than a Dublin local authority or a region. It is an index of price changes, not the price of any one home.&lt;/p&gt;

&lt;p&gt;Rents are averages of registered tenancies for an area, not the rent of any one home. The CSO tables are the authority, and the data is published some months after the period it covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two modes
&lt;/h2&gt;

&lt;p&gt;Choose the table with &lt;code&gt;table&lt;/code&gt; (&lt;code&gt;RIQ02&lt;/code&gt; or &lt;code&gt;HPM09&lt;/code&gt;) and the job with &lt;code&gt;mode&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search&lt;/code&gt; returns rows for what you filter. For rent: &lt;code&gt;locations&lt;/code&gt;, &lt;code&gt;bedrooms&lt;/code&gt;, &lt;code&gt;propertyTypes&lt;/code&gt;, &lt;code&gt;quarterFrom&lt;/code&gt; and &lt;code&gt;quarterTo&lt;/code&gt; (for example &lt;code&gt;2024Q1&lt;/code&gt;). For the price index: category names in &lt;code&gt;locations&lt;/code&gt;, &lt;code&gt;statistics&lt;/code&gt; (&lt;code&gt;index&lt;/code&gt;, &lt;code&gt;change1m&lt;/code&gt;, &lt;code&gt;change3m&lt;/code&gt;, &lt;code&gt;change12m&lt;/code&gt;, which are the CSO's own published changes), &lt;code&gt;monthFrom&lt;/code&gt; and &lt;code&gt;monthTo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lookup&lt;/code&gt; gives the latest figure and the change on a year earlier, for 1 to 20 places (rent) or categories (price index).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Values are spelled as the table spells them, for example &lt;code&gt;Carlow&lt;/code&gt;, &lt;code&gt;Two bed&lt;/code&gt;, &lt;code&gt;Apartment&lt;/code&gt;. A bedrooms or property type value the table does not have refuses the run before anything is charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a row looks like
&lt;/h2&gt;

&lt;p&gt;A rent lookup for Carlow, all bedrooms and all property types, run on 28 Oct 2026, returned one row. Selected fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Carlow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bedrooms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All bedrooms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propertyType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All property types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;latestQuarter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2025Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;latestAverageMonthlyRentEur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1349.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yearEarlierQuarter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2024Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;changePercent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RIQ02&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;changePercent&lt;/code&gt; is worked out from the two published averages and rounded to one decimal place.&lt;/p&gt;

&lt;p&gt;A price index lookup for &lt;code&gt;Dublin - all residential properties&lt;/code&gt; in the same test returned a latest index of 178.4 for August 2026, against 171.8 a year earlier, with the CSO's published 12-month change of 3.9%.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many rows a lookup returns
&lt;/h2&gt;

&lt;p&gt;This is the one thing to know before you run it. A rent lookup returns one row per place, bedrooms group and property type. That is up to 42 rows per place (7 bedrooms groups x 6 property types), fewer where the source has no figure. In a test on 28 Oct 2026, a lookup of 2 places with no filters returned 68 rows.&lt;/p&gt;

&lt;p&gt;If you want one row per place, set the filters to the totals:&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;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RIQ02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lookup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locations"&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;"Carlow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Carlow Town"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bedrooms"&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;"All bedrooms"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"propertyTypes"&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;"All property types"&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 price index lookup returns one row per category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price
&lt;/h2&gt;

&lt;p&gt;One event, &lt;code&gt;row&lt;/code&gt;: $0.001 per row returned, from either table, flat on every Apify plan. There is no start fee and no run fee. A refused run, an empty result and a source problem are not charged. &lt;code&gt;maxItems&lt;/code&gt; (default 100) caps the rows you can be charged for, and the run's maximum charge stops it at the last row it can pay for.&lt;/p&gt;

&lt;p&gt;Worked examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A rent lookup of 2 places with no filters: 68 rows, $0.068.&lt;/li&gt;
&lt;li&gt;The same lookup filtered to all bedrooms and all property types: 2 rows, $0.002.&lt;/li&gt;
&lt;li&gt;A price index lookup of 2 categories: 2 rows, $0.002.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Calling it from Python
&lt;/h2&gt;

&lt;p&gt;With the &lt;code&gt;apify-client&lt;/code&gt; package (3.x), set a maximum charge on the run so a wide query cannot cost more than you expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/ireland-rent-index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RIQ02&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Carlow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrooms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All bedrooms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propertyTypes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All property types&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;max_total_charge_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.05&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latestQuarter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latestAverageMonthlyRentEur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changePercent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each run also saves a &lt;code&gt;RUN_SUMMARY&lt;/code&gt; record in its key-value store: the status, the table, the rows matched and returned, the empty cells left out, any places not found in a lookup, and the attribution lines below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One request to the CSO per run (the whole table), at most one request every 1.5 seconds, a 60-second network timeout, and reading the source stops after 5 minutes.&lt;/li&gt;
&lt;li&gt;A rate-limit refusal (HTTP 429 or 503) stops the run with no retry and nothing charged; &lt;code&gt;RUN_SUMMARY.retryAfter&lt;/code&gt; says when to try again.&lt;/li&gt;
&lt;li&gt;The input takes no URLs, headers or query strings: the Actor builds the request itself.&lt;/li&gt;
&lt;li&gt;Nothing is read from the RTB website; only the CSO's published tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credit
&lt;/h2&gt;

&lt;p&gt;The data is CC BY 4.0, and the Actor's output carries these lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: RTB Average Monthly Rent Report (table RIQ02), copyright Residential Tenancies Board (RTB), published by the Central Statistics Office, Ireland, licensed under Creative Commons Attribution 4.0 (CC BY 4.0). Changes were made: cells with no figure in the source (insufficient data) are not returned, and rows are filtered and reshaped.&lt;/li&gt;
&lt;li&gt;Source: Residential Property Price Index (table HPM09), copyright Central Statistics Office, Ireland, licensed under Creative Commons Attribution 4.0 (CC BY 4.0). Changes were made: the table is filtered and reshaped into one row per month, category and statistic; cells with no figure are not returned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rows hold area averages and index values: a place or category, a period, bedrooms and property type, and the figure. There are no fields for people, addresses, landlords, tenants or properties.&lt;/p&gt;

&lt;p&gt;If you try it, the Store page is here: &lt;a href="https://apify.com/yappman/ireland-rent-index" rel="noopener noreferrer"&gt;yappman/ireland-rent-index&lt;/a&gt;. Ratings and reviews there help other buyers judge whether it fits their use.&lt;/p&gt;

</description>
      <category>python</category>
      <category>apify</category>
      <category>opendata</category>
      <category>ireland</category>
    </item>
    <item>
      <title>Six months, 6,314 real bets, +1.2%: what a betting bot taught me about data</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:32:34 +0000</pubDate>
      <link>https://dev.to/yappman/six-months-6314-real-bets-12-what-a-betting-bot-taught-me-about-data-kff</link>
      <guid>https://dev.to/yappman/six-months-6314-real-bets-12-what-a-betting-bot-taught-me-about-data-kff</guid>
      <description>&lt;p&gt;I spent six months building a bot that placed bets on the Betfair Exchange. On 29 October 2026 I stopped live betting, paused development and switched the bot off. This is what I learned, and it is mostly not about betting.&lt;/p&gt;

&lt;p&gt;The short version: the bot's results looked good in testing, did not follow in live running, and almost every reason I found was a data or measurement error. The models were rarely the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;A Betfair Exchange bot across many sports: tennis, horse racing, football, darts, cricket, golf, basketball and more. It had paper trading, an optimiser that generated thousands of model variants, and live execution at small stakes (£1 to £5). I built it with AI coding agents (Claude Code).&lt;/p&gt;

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

&lt;p&gt;From 22 April to 29 October 2026 the live book settled 6,314 bets with £16,792 staked, for a net +£205 (+1.2%).&lt;/p&gt;

&lt;p&gt;By month: April −£99 (−16.7%), May +£487 (+5.8%), June −£221 (−4.3%), July −£11 (−1.0%), August +£60 (+8.7%), September −£36 (−11.5%), October +£26 (+5.5%).&lt;/p&gt;

&lt;p&gt;All of the profit came from May. Without May the total is about −£280. After I reset to small flat stakes in July, the result was about +£38 on roughly £2,600 staked. That is inside one standard deviation of luck, about ±£144, so it is indistinguishable from zero. The last 60 days came to about −£23, and tennis, the core of the book, ran −7.6%.&lt;/p&gt;

&lt;p&gt;I did not find a live edge. So I stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lessons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Paper beats live by a lot, and it is not commission
&lt;/h3&gt;

&lt;p&gt;For the same lane, the gap between paper and live return on stake (ROI) measured 6 to 19 points. Commission explained only about 0 to 1 point. Almost all of the rest was adverse selection: you get filled when you are wrong. Paper ROI also shrank by 60% or more out of sample.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Execution was the edge, not prediction
&lt;/h3&gt;

&lt;p&gt;Across the book, only 18.5% of live orders were filled. For tennis, crossing the spread lifted the fill rate from 37.8% to 92.5%. The price advantage vanished: median closing-line value went to 0.00%, and live P&amp;amp;L ran −£61 in six days. The apparent edge had been queue position, not selection.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Phantom settlements
&lt;/h3&gt;

&lt;p&gt;Bets that never matched on the exchange were settled anyway, using the market result. That created fake live P&amp;amp;L. I excluded 25 of them in July. It came back in October through a different code path: 31 bets that lapsed on Betfair were settled (−£9.38).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Survivor bias from a clock race
&lt;/h3&gt;

&lt;p&gt;12.3k bets got stuck because settlement compared against an event clock that raced the market closing. The bets that did settle were a biased sample. One Formula 1 lane showed +242% on 6 bets. Its true figure was +1% on 52, and I had already put it live on the strength of the inflated one.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Counting the same bet many times
&lt;/h3&gt;

&lt;p&gt;Hundreds of model variants placed the same bet on the same selection, and I counted each result as independent evidence. When I de-duplicated by market, selection and direction, apparently significant results collapsed. One "strong" cell fell to 48 bets with a confidence interval that spanned zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Optimisers find lucky days, not edges
&lt;/h3&gt;

&lt;p&gt;71% of the high-ROI lanes that had a live record lost money live. Their backtest profit was concentrated in a handful of days. Screening by how concentrated the profit was across days worked better than screening by ROI or t-statistics.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Closing-line value did not predict profit
&lt;/h3&gt;

&lt;p&gt;The usual rule says that if you beat the closing price, profit follows. Across my whole book the correlation between closing-line value and ROI was about −0.10. The one lane that made money live did not beat the sharp close. It was harvesting a market inefficiency.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. A config save silently deleted 1,306 lanes
&lt;/h3&gt;

&lt;p&gt;A configuration save removed 1,306 strategy lanes, including 12 of the 20 live ones, and left no log trail. I found it weeks later because performance had changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Growing stakes on winning streaks buys high
&lt;/h3&gt;

&lt;p&gt;Raising stakes during winning runs meant the drawdown arrived at peak stake size. Flat stakes were safer.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. A horse's name broke the API client
&lt;/h3&gt;

&lt;p&gt;A runner with "ssl" in its name made a parse error look like a network error. That triggered bursts of "API paused" back-off.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. The tests wrote to the live database
&lt;/h3&gt;

&lt;p&gt;Running the test suite in the production environment leaked test bets into the real ledger.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. Nearly everything tested negative
&lt;/h3&gt;

&lt;p&gt;I tested 12 conditioning filters (surface, time of day, fatigue, venue and others): 11 were dead and 1 was untestable. I tried 25 more sports and markets, and every one was refuted. An LLM "advisor" that picked bets came out significantly negative (t = −2.91), and it simply copied the favourite 99.35% of the time. Switching venue was not an option either: Polymarket and Kalshi are blocked in the UK.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it was really about: what was known when
&lt;/h2&gt;

&lt;p&gt;Look back over that list. Most of it is a timing or provenance failure. Using information the bot could not have had at decision time. Clocks racing each other. UTC against BST. Dead heats. Partial updates. Duplicates counted as independent evidence.&lt;/p&gt;

&lt;p&gt;Results that look like an edge are very often a data artefact. Whatever you are modelling, whether it is prices, property or sport, you need data that records what was known when, not just what is true now.&lt;/p&gt;

&lt;p&gt;That is why I publish dated, point-in-time data at &lt;a href="https://data.yappman.com" rel="noopener noreferrer"&gt;data.yappman.com&lt;/a&gt;: UK and Australian shelf prices (daily and weekly snapshots, corrected only for errors), plus property, hotel and public-contract data through Apify.&lt;/p&gt;

&lt;p&gt;The bot is switched off; the code and data are archived.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing here is betting advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>python</category>
      <category>testing</category>
      <category>lessons</category>
    </item>
    <item>
      <title>Publishing data from UK public sources without publishing people</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:28:41 +0000</pubDate>
      <link>https://dev.to/yappman/publishing-data-from-uk-public-sources-without-publishing-people-5ca8</link>
      <guid>https://dev.to/yappman/publishing-data-from-uk-public-sources-without-publishing-people-5ca8</guid>
      <description>&lt;p&gt;UK public bodies publish a lot of open data. An open licence covers the dataset's copyright. It does not make personal data in that dataset safe to republish. Both the HMRC trade lists and the Global LEI Index contain records that are really people. This article looks at how three of my Apify Actors deal with that. It describes what each Store README says, and nothing more. This is not legal advice.&lt;/p&gt;

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

&lt;p&gt;A sole trader trades under their own name. A register that lists "importers and exporters" or "legal entities" will therefore list some individuals next to companies. A tender notice can name a contact. A name can sit inside a company name. If you collect these sources into one tidy table, you can end up publishing people without meaning to.&lt;/p&gt;

&lt;p&gt;The three Actors take different routes. All three are pay per event, and the Store pages carry the current prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  UK Importers and Exporters (HMRC Trade Info)
&lt;/h2&gt;

&lt;p&gt;This Actor returns one row per trader, flow and month. HMRC publishes sole traders' names next to companies', and the README says the Actor does not return them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Organisations only.&lt;/strong&gt; A name is returned only if it carries an organisation marker, such as Limited, Ltd, PLC or LLP. A name without one is withheld. So is a name with a title such as Mr or Dr, unless a legal form follows. So is a name that holds a contact or an address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counted, not silent.&lt;/strong&gt; In search and alert modes, withheld names are left out and counted in &lt;code&gt;RUN_SUMMARY.withheldPersons&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No addresses.&lt;/strong&gt; Address lines are never output or stored. Only the outward part of the postcode is returned, and there is no address field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing kept.&lt;/strong&gt; Every run reads HMRC's current publication, and no trader name or address is kept between runs. An alert's store holds hashes only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The licence is the Open Government Licence v3.0. The README adds that the licence does not cover personal data, so sole traders' details are not ours to republish.&lt;/p&gt;

&lt;h2&gt;
  
  
  UK Public Contracts Lookup
&lt;/h2&gt;

&lt;p&gt;This Actor returns notices from Find a Tender and Contracts Finder (and Public Contracts Scotland if you choose it) in one schema. The README describes which fields a row can hold and which it never holds. That is a design choice at the field level.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No supplier names.&lt;/strong&gt; A row holds the title, buyer, value, CPV codes, regions, stage, dates and a link. It has no winner, contact, address or description field, and the buyer field is the organisation name only. A &lt;code&gt;suppliers&lt;/code&gt; field is reserved for organisation supplier names, pending a review; it is null in this version, and no supplier name is read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing else that identifies a person.&lt;/strong&gt; Rows hold no contact names, emails, phone numbers, addresses, supplier names or descriptions. Keywords are matched against the description, but the description is never returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redaction in titles.&lt;/strong&gt; Email addresses, UK phone numbers and a name written straight after words such as "contact", "speak to" or "FAO" are replaced with &lt;code&gt;[redacted]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README is open about the limit: the redaction works by pattern, so a name written some other way in a title is not caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  LEI Lookup
&lt;/h2&gt;

&lt;p&gt;This Actor returns Legal Entity Identifier records from the Global LEI Index, one row per entity. The data is CC0. The README notes that CC0 applies to the dataset's copyright and does not remove data-protection duties for personal data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sole proprietors withheld.&lt;/strong&gt; A recognised sole proprietor is a record with the category &lt;code&gt;SOLE_PROPRIETOR&lt;/code&gt;, or one registered under a sole-trader legal form (98 codes in the ISO 20275 list). Arrangements named after a person are treated the same way: settlements, family, will, discretionary and life-interest trusts, and "estate of".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two behaviours.&lt;/strong&gt; In lei mode, the row keeps the LEI, statuses and dates, but the names, registration number and addresses are &lt;code&gt;null&lt;/code&gt;, and the &lt;code&gt;withheld&lt;/code&gt; field says which. In name and status modes, these entities are left out and counted in &lt;code&gt;RUN_SUMMARY.withheldPersons&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;City-level addresses.&lt;/strong&gt; For everyone, an address is country, region and city only. No street, number or postcode is read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README states two limits. A company whose name contains a person's name is returned as published. And the sole-proprietor and arrangement tests can miss a person. The record link opens the source record, which the Actor does not filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the three have in common
&lt;/h2&gt;

&lt;p&gt;Each one decides at the field level what a row can hold, rather than cleaning up afterwards. Each one counts what it withheld, so a result with fewer rows than expected is explained. And each README says where its method stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removal and objection
&lt;/h2&gt;

&lt;p&gt;The Trade Info README sets out a route. HMRC removes a trader who asks, from the following month's publication, and the Actor stops returning it from then. You can also ask us directly. Email &lt;a href="mailto:info@yappman.com"&gt;info@yappman.com&lt;/a&gt; with the trader's name as it appears in the results, or its HMRC trader id. The README says requests get a reply within one month, and that each accepted request is a reviewed change to our own removal list. Traders on it are never returned, and are counted in &lt;code&gt;RUN_SUMMARY.removedByRequest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Contracts and LEI READMEs give the same route: email &lt;a href="mailto:info@yappman.com"&gt;info@yappman.com&lt;/a&gt; with the notice id or the LEI, or, without one, the product and the name as it appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who owns the output
&lt;/h2&gt;

&lt;p&gt;Our position is that output belongs to the account that runs the Actor, under Apify's terms. That account is then responsible for what it does with the rows. Again, this is not legal advice. If you are unsure whether a use is lawful, ask a qualified adviser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Store pages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UK Importers and Exporters: &lt;a href="https://apify.com/yappman/uk-importers-exporters" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-importers-exporters&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;UK Public Contracts Lookup: &lt;a href="https://apify.com/yappman/contracts-lookup" rel="noopener noreferrer"&gt;https://apify.com/yappman/contracts-lookup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LEI Lookup: &lt;a href="https://apify.com/yappman/lei-lookup" rel="noopener noreferrer"&gt;https://apify.com/yappman/lei-lookup&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drafted with an AI assistant. The behaviour described comes from the three Store READMEs as they stood on the afternoon of 28 October 2026.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>privacy</category>
      <category>uk</category>
      <category>apify</category>
    </item>
    <item>
      <title>Notify search engines on every GitHub Pages deploy with IndexNow</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:29:47 +0000</pubDate>
      <link>https://dev.to/yappman/notify-search-engines-on-every-github-pages-deploy-with-indexnow-52of</link>
      <guid>https://dev.to/yappman/notify-search-engines-on-every-github-pages-deploy-with-indexnow-52of</guid>
      <description>&lt;p&gt;When I looked on 27 October 2026, Bing Webmaster Tools no longer showed a URL Submission page. It pointed to IndexNow instead. So I added a small GitHub Actions workflow to the repository behind data.yappman.com. After each GitHub Pages deploy, it tells IndexNow which pages changed. This article walks through it. It is our own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What IndexNow needs
&lt;/h2&gt;

&lt;p&gt;IndexNow lets a site tell participating search engines that URLs have changed, instead of waiting for them to find out. You publish a key as a text file at your site root. The file is named after the key and contains the key.&lt;/p&gt;

&lt;p&gt;That key is public by design. It is not a secret and does not belong in repository secrets. Each request names the file's address in a &lt;code&gt;keyLocation&lt;/code&gt; field, so the receiver can fetch it. In our repository the workflow finds the key by looking for a 32-character hex name ending in &lt;code&gt;.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why only the changed pages? Sending every URL on every deploy tells a search engine nothing about what is new. A diff keeps each submission to what changed. The workflow also needs no secret and no token. Its permissions are &lt;code&gt;contents: read&lt;/code&gt; and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trigger and the gate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages-build-deployment"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Git&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ref&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;compare&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(default:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;HEAD)"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow runs when GitHub's own &lt;code&gt;pages-build-deployment&lt;/code&gt; workflow completes. You can also start it by hand, with an optional base ref to compare against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.INDEXNOW_ENABLED == 'true' &amp;amp;&amp;amp; (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The job runs only if the repository variable &lt;code&gt;INDEXNOW_ENABLED&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;. Within that, a manual start always passes, and a deploy-triggered start passes only if the deploy succeeded. The file's own header calls it a draft that does nothing until you set the variable (Settings, Secrets and variables, Actions, Variables).&lt;/p&gt;

&lt;p&gt;The job checks out the deployed commit with full history (&lt;code&gt;fetch-depth: 0&lt;/code&gt;), because the next step needs to compare commits. A &lt;code&gt;concurrency&lt;/code&gt; group keeps two pings from running at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work out the changed pages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"HEAD~1"&lt;/span&gt;
git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; HEAD &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; changed.txt
node docs/release/indexnow-urls.js &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;changed.txt&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; urls.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no base given, it compares against the commit before HEAD. &lt;code&gt;git diff --name-only&lt;/code&gt; lists the changed &lt;code&gt;.html&lt;/code&gt; files. A small Node script then turns file paths into public URLs.&lt;/p&gt;

&lt;p&gt;The script is short. It accepts only &lt;code&gt;.html&lt;/code&gt; paths. It skips &lt;code&gt;_held/&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;assets/&lt;/code&gt; and &lt;code&gt;.github/&lt;/code&gt;, the 404 page and the Google verification file. &lt;code&gt;index.html&lt;/code&gt; becomes the site root. &lt;code&gt;folder/index.html&lt;/code&gt; becomes &lt;code&gt;https://data.yappman.com/folder/&lt;/code&gt;. It removes duplicates, sorts the list and prints one URL per line. The step then writes the count of URLs to the step output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ping IndexNow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KEY_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^[0-9a-f]{32}\.txt$'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEY_FILE&lt;/span&gt;&lt;span class="p"&gt;%.txt&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"https://data.yappman.com/&lt;/span&gt;&lt;span class="nv"&gt;$KEY_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step runs only if the count is not zero. It finds the key file, then fetches it from the live site and checks the content matches the key. If the file is not served, the step fails before anything is sent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.indexnow.org/indexnow"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json; charset=utf-8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @body.json &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;HTTP %{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A short inline Node command builds &lt;code&gt;body.json&lt;/code&gt; with the host, the key, the key location and the URL list. Then &lt;code&gt;curl&lt;/code&gt; posts it to &lt;code&gt;https://api.indexnow.org/indexnow&lt;/code&gt; and prints the HTTP status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;INDEXNOW_ENABLED&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, then start the workflow by hand from the Actions tab. Leave &lt;code&gt;base&lt;/code&gt; empty to compare with the previous commit. Or give it an older ref, such as a tag, to cover several deploys in one go. Read the log. The changed-pages step prints the URL list, and the ping step ends with the HTTP status.&lt;/p&gt;

&lt;h2&gt;
  
  
  First run
&lt;/h2&gt;

&lt;p&gt;Our first run submitted 20 changed URLs and got HTTP 202. I read that as accepted for processing. It does not mean the pages are indexed. Check your own search engine tools later to see what they did with the URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IndexNow is a hint, not a guarantee. A search engine decides whether and when to crawl and index a page.&lt;/li&gt;
&lt;li&gt;Google has not announced support for IndexNow. For Google, the sitemap and Search Console remain the routes.&lt;/li&gt;
&lt;li&gt;The workflow only sees &lt;code&gt;.html&lt;/code&gt; files that follow the &lt;code&gt;folder/index.html&lt;/code&gt; pattern. Changes to other file types are not submitted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git diff --name-only&lt;/code&gt; also lists deleted files, and the script does not check that a file still exists. A removed page would be submitted as a changed URL.&lt;/li&gt;
&lt;li&gt;If a deploy changes nothing in HTML, nothing is sent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the site
&lt;/h2&gt;

&lt;p&gt;data.yappman.com publishes a free weekly UK and Australian shelf-price index. Product-level shelf prices on demand come from the UK Supermarket Price Scraper on the Apify Store: &lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-supermarket-price-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drafted with an AI assistant. The workflow described is our own code, and the figures come from our own first run on 27 October 2026.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>seo</category>
      <category>bing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Get UK supermarket prices into an AI assistant via MCP: a free index server and a paid full-data route</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:07:00 +0000</pubDate>
      <link>https://dev.to/yappman/get-uk-supermarket-prices-into-an-ai-assistant-via-mcp-a-free-index-server-and-a-paid-full-data-4l7o</link>
      <guid>https://dev.to/yappman/get-uk-supermarket-prices-into-an-ai-assistant-via-mcp-a-free-index-server-and-a-paid-full-data-4l7o</guid>
      <description>&lt;p&gt;If you ask an AI assistant what groceries cost, it will usually guess or search the web. I would rather it read a dataset. The Model Context Protocol (MCP) lets an assistant call tools on a remote server, so a price dataset can be one of those tools.&lt;/p&gt;

&lt;p&gt;This article covers two routes. One is a free MCP server I run that serves a weekly shelf-price index and a small fixed basket. The other is Apify's hosted MCP server, which lets an assistant run two pay-per-event Actors that return product-level shelf prices on request. The free route is static snapshots and cannot answer every question. The paid route exists for the questions it cannot.&lt;/p&gt;

&lt;p&gt;The free server has its own page, with the endpoint, tools and licence: &lt;a href="https://data.yappman.com/mcp/" rel="noopener noreferrer"&gt;https://data.yappman.com/mcp/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The free index and basket are a small sample. Product-level data, on request, comes from the two paid Actors covered further down.&lt;/p&gt;

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

&lt;p&gt;An assistant asked "did UK shelf prices go up this week?" has three bad options: recall something from training, search the web and quote a news article, or make up a figure. None of them gives you a dated source you can cite.&lt;/p&gt;

&lt;p&gt;A tool call fixes the first part. The assistant asks a server for a specific week, gets rows back, and can say which week and where it came from. That is the whole idea.&lt;/p&gt;

&lt;p&gt;For background on the wider choice of UK supermarket price sources, free and paid, I compared them in an earlier article: &lt;a href="https://dev.to/yappman/where-to-get-uk-supermarket-price-data-free-and-paid-compared-h3p"&gt;Where to get UK supermarket price data: free and paid, compared&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free server
&lt;/h2&gt;

&lt;p&gt;It is a small read-only server on Cloudflare Workers. It uses streamable HTTP and needs no key or account.&lt;/p&gt;

&lt;p&gt;The address is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yappman-grocery-mcp.blue-bar-ca29.workers.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is also listed in the MCP Registry as &lt;code&gt;io.github.yappmaniac/grocery-price-snapshots&lt;/code&gt;, and described at &lt;a href="https://data.yappman.com/mcp/" rel="noopener noreferrer"&gt;https://data.yappman.com/mcp/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What it serves, all under CC BY 4.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The weekly UK and Australian online shelf-price index. The Australian index includes Coles and Woolworths rows from my own collection runs.&lt;/li&gt;
&lt;li&gt;A fixed 212-item UK basket, with price, was-price, loyalty price, unit price, pack size and stock flag per retailer, on the days it was collected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Product names, brands, descriptions and images are not served. Those belong to the retailers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add it in Claude
&lt;/h3&gt;

&lt;p&gt;On a Free, Pro, Max, Team or Enterprise plan, Claude can use a remote MCP server as a custom connector. On Pro or Max:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Customize, then Connectors.&lt;/li&gt;
&lt;li&gt;Choose Add, then Add custom connector.&lt;/li&gt;
&lt;li&gt;Enter a name (for example "UK shelf prices") and the address above, then Continue.&lt;/li&gt;
&lt;li&gt;Under Authentication, Claude should detect "No sign-in"; leave it, then Add.&lt;/li&gt;
&lt;li&gt;In a chat, use the "+" button at the lower left, choose Connectors, and switch the new one on for that conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On Team and Enterprise plans an owner adds the connector under Organization settings first, and each person then connects it. Free accounts are limited to one custom connector, which matters later in this article.&lt;/p&gt;

&lt;p&gt;These steps follow Anthropic's help page "Get started with custom connectors using remote MCP", which I read on 27 October 2026. Menu labels change, so if yours differ, go by that page: &lt;a href="https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp" rel="noopener noreferrer"&gt;https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you use Claude Code, this command should add it (I tested the Claude.ai route above, not this one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http supermarket-prices https://yappman-grocery-mcp.blue-bar-ca29.workers.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add it in ChatGPT
&lt;/h3&gt;

&lt;p&gt;OpenAI documents how to connect your own MCP server to ChatGPT in its developer mode help article: &lt;a href="https://help.openai.com/en/articles/12584461-developer-mode-apps-and-full-mcp-connectors-in-chatgpt-beta" rel="noopener noreferrer"&gt;https://help.openai.com/en/articles/12584461-developer-mode-apps-and-full-mcp-connectors-in-chatgpt-beta&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have not tried it, and which plans and workspaces can do it, and the menu names, are set by OpenAI and may change, so I am not giving steps here. Follow that page. The free server's address is the one given above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example questions and the tool that answers each
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You ask&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Which weeks of the UK index are published?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list_index_vintages&lt;/code&gt; with &lt;code&gt;market: "uk"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Newest first, with publication time, revision and base week.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How did UK posted shelf prices move this week?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_shelf_price_index&lt;/code&gt; with &lt;code&gt;market: "uk"&lt;/code&gt; and &lt;code&gt;recordType: "headline"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Omit &lt;code&gt;vintage&lt;/code&gt; for the latest week. Returns the index level and week-on-week change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which Australian retailer's shelf prices moved most last week?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_shelf_price_index&lt;/code&gt; with &lt;code&gt;market: "au"&lt;/code&gt;, then compare the retailer rows&lt;/td&gt;
&lt;td&gt;Retailer ids are &lt;code&gt;coles&lt;/code&gt;, &lt;code&gt;woolworths&lt;/code&gt; and &lt;code&gt;aldi_au&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show dairy and eggs for one UK retailer.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_shelf_price_index&lt;/code&gt; with &lt;code&gt;market: "uk"&lt;/code&gt;, &lt;code&gt;retailer: "tesco"&lt;/code&gt; and &lt;code&gt;class: "dairy-eggs"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;class&lt;/code&gt; takes a product class id.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What did this barcode cost at the UK retailers that carry it on a given day?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lookup_basket_prices&lt;/code&gt; with &lt;code&gt;eans&lt;/code&gt; and &lt;code&gt;date&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Up to 20 barcodes per call. Only the 212 basket products.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What did a particular Aldi product cost?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lookup_basket_prices&lt;/code&gt; with &lt;code&gt;products: [{"retailer": "aldi", "retailerProductId": "..."}]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The basket holds no barcodes for Aldi, so use the retailer's product id.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can I cite this? What is the licence and which days are missing?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;about_the_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns the licence, what is not served, the published weeks and the basket date range with short and missing days.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each result from &lt;code&gt;get_shelf_price_index&lt;/code&gt; and &lt;code&gt;lookup_basket_prices&lt;/code&gt; carries a credit line. A well-behaved assistant will repeat it. You can ask it to.&lt;/p&gt;

&lt;p&gt;A prompt to try: "Use the UK shelf prices connector. Call about_the_data first, then get the latest headline index and tell me the week, the week-on-week change and the credit line."&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need more: the paid Actors via Apify's MCP server
&lt;/h2&gt;

&lt;p&gt;The free server has no product names, no search and no live lookups. Questions like "what does this product cost at four retailers right now" or "price my shopping list" need product-level data. I publish two Actors on the Apify Store for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;UK Supermarket Price Scraper&lt;/a&gt; covers Tesco, Sainsbury's, Asda and Aldi.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/yappman/au-supermarket-price-scraper" rel="noopener noreferrer"&gt;AU Supermarket Price Scraper&lt;/a&gt; covers Aldi Australia. (It can also read Coles and Woolworths, but only for runs on my own account; for any other account those two are skipped and not charged.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Store pages are the place for current pricing and what each mode does. Both are pay per event, charged to the Apify account that runs them. The Actors return current product-level data on request: the run happens when your assistant asks for it.&lt;/p&gt;

&lt;p&gt;Apify hosts an MCP server at &lt;code&gt;https://mcp.apify.com&lt;/code&gt;. A &lt;code&gt;tools&lt;/code&gt; parameter in the address limits which Actors it offers as tools. It does not limit what your sign-in can reach: the client can read other runs, datasets and stores in your Apify account. If that matters, use a separate Apify account for this. Actors are written as owner/name and separated by commas. For the UK Actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mcp.apify.com?tools=yappman/uk-supermarket-price-scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have connected to that address from Claude as a custom connector: it signs in through Apify and lists the Actor and four helper tools. Apify's documentation shows several Actors in one &lt;code&gt;tools&lt;/code&gt; list separated by commas, so the address for both is &lt;code&gt;?tools=yappman/uk-supermarket-price-scraper,yappman/au-supermarket-price-scraper&lt;/code&gt;. I have connected with the Australian Actor in the list, but not with this exact address.&lt;/p&gt;

&lt;p&gt;The four helper tools are Get Actor run, Get dataset items, Get key-value store record and Abort Actor run. Claude asks for approval before each tool call unless you change that setting. That is a client setting: if you allow a tool permanently, or use a client that runs tools automatically, Actor runs will start, and be charged, without asking.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;about_the_data&lt;/code&gt; tool on the free server points to the two Actors and, in a &lt;code&gt;fullerData&lt;/code&gt; field, to that combined address, so an assistant that hits the limits of the free data can point you there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create or sign in to an Apify account. Before connecting, set a monthly usage limit on that Apify account. Every run is charged to your account, not mine. Running these Actors costs money, so check the pricing on each Store page first.&lt;/li&gt;
&lt;li&gt;Add the address above to your client as a remote MCP server. In Claude, that is the same custom connector route as before, using the Apify address. Free Claude accounts are limited to one custom connector, so you would need to swap out the free server or use a paid plan to have both at once.&lt;/li&gt;
&lt;li&gt;Sign in with Apify when asked. Apify's documentation says OAuth is the recommended way to connect: your browser opens so you can sign in and approve access, and your API token stays out of the client configuration. Clients without OAuth support can send an Apify API token in an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header instead; use a scoped token and keep it out of shared or committed config.&lt;/li&gt;
&lt;li&gt;Ask for what you want: "Use the UK supermarket Actor to find current shelf prices for semi-skimmed milk at Tesco, Asda and Aldi." Check the Actor call your client shows before you approve it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Apify's MCP page (read 27 October 2026) gives no steps for Claude web connectors, so the Claude steps here come from my own test and Anthropic's help page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and licence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Posted online shelf prices, not CPI, not prices paid.&lt;/strong&gt; The index is not a government statistic and does not reflect what shoppers paid at the till.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static snapshots, not live.&lt;/strong&gt; The free server serves compiled files. It may be incomplete or out of date. Check the retailer before relying on a price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free hosting.&lt;/strong&gt; The free server runs on a free plan. Heavy use may be turned away for the rest of the day. Please do not crawl it or build production systems on it, and I may change or remove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage.&lt;/strong&gt; The basket is 212 products. The index has no product names. Anything wider is the paid route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licence.&lt;/strong&gt; My compilation is CC BY 4.0. I claim no rights in retailer content, and none is served. Credit it as: "Source: Yappman Data, [dataset title], [vintage or date], CC BY 4.0." The index and basket tools return the exact line. Say if you changed anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output of the paid Actors&lt;/strong&gt; belongs to the Apify account that runs them, under Apify's terms. Retailer content in it (images, descriptions) is not mine to license. This is not legal advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independence.&lt;/strong&gt; Yappman Data is not affiliated with or endorsed by any retailer, government body or Apify. Provided as is, with no warranty.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Free MCP server page: &lt;a href="https://data.yappman.com/mcp/" rel="noopener noreferrer"&gt;https://data.yappman.com/mcp/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Endpoint: &lt;a href="https://yappman-grocery-mcp.blue-bar-ca29.workers.dev/mcp" rel="noopener noreferrer"&gt;https://yappman-grocery-mcp.blue-bar-ca29.workers.dev/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP Registry name: &lt;code&gt;io.github.yappmaniac/grocery-price-snapshots&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;UK Actor: &lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-supermarket-price-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AU Actor: &lt;a href="https://apify.com/yappman/au-supermarket-price-scraper" rel="noopener noreferrer"&gt;https://apify.com/yappman/au-supermarket-price-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apify's MCP documentation: &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;https://docs.apify.com/platform/integrations/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Earlier comparison article: &lt;a href="https://dev.to/yappman/where-to-get-uk-supermarket-price-data-free-and-paid-compared-h3p"&gt;https://dev.to/yappman/where-to-get-uk-supermarket-price-data-free-and-paid-compared-h3p&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Contact: &lt;a href="mailto:info@yappman.com"&gt;info@yappman.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a tool returns something wrong or confusing, tell me in the comments.&lt;/p&gt;

&lt;p&gt;Drafted with an AI assistant. The tests described were run on my own accounts on 27 October 2026, some by the assistant under my direction.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>opendata</category>
      <category>apify</category>
    </item>
    <item>
      <title>Finding UK importers of a product by HS code with HMRC's open data</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:27:48 +0000</pubDate>
      <link>https://dev.to/yappman/finding-uk-importers-of-a-product-by-hs-code-with-hmrcs-open-data-4977</link>
      <guid>https://dev.to/yappman/finding-uk-importers-of-a-product-by-hs-code-with-hmrcs-open-data-4977</guid>
      <description>&lt;p&gt;Not affiliated with HMRC. The data comes from HMRC UK Trade Info, published under the Open Government Licence v3.0.&lt;/p&gt;

&lt;p&gt;If you sell something, source something or research a market, a common question is who in the UK already imports or exports it. HMRC publishes monthly lists of importers and exporters by commodity code, but the raw files are awkward: one file per month, codes at several lengths, and names mixed with sole traders.&lt;/p&gt;

&lt;p&gt;This post goes from a product to a list of UK traders using HS and CN8 codes. It covers choosing a code, searching by code, flow and month, and reading the output in Python. The rows come from an Apify Actor I built, &lt;a href="https://apify.com/yappman/uk-importers-exporters" rel="noopener noreferrer"&gt;UK Importers and Exporters: HMRC Trade Data&lt;/a&gt;. It returns one row per trader, flow and month, with the outward code of the postcode only and no address. A name is kept only if it carries an organisation marker such as Limited, Ltd, PLC or LLP, so sole traders are left out. Pricing is pay per event: $0.005 a run plus $0.002 per trader row.&lt;/p&gt;

&lt;p&gt;This is a list of companies from public records. It tells you nothing about volumes or values.&lt;/p&gt;

&lt;p&gt;To follow along without running anything, there is a free &lt;a href="https://www.kaggle.com/datasets/russellyapp/uk-importers-and-exporters-hs-8419-aug-2026" rel="noopener noreferrer"&gt;20-row sample on Kaggle&lt;/a&gt; (HS 8419, August 2026). The rows are in name order from the start of the list, so it is not a random or representative sample of the month. If you want to run a search yourself, there is a ready-made &lt;a href="https://apify.com/yappman/uk-importers-exporters/examples/uk-importers-of-heating-and-cooling-machinery" rel="noopener noreferrer"&gt;example Task&lt;/a&gt;: HS 8419, imports, 20 rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What the rows contain, and what they do not
&lt;/h2&gt;

&lt;p&gt;Each row says that a trader dealt in goods under some commodity codes in a given month, in one flow: imports or exports. The row carries the 8-, 6- and 4-digit commodity codes that trader dealt in, and the outward code of its postcode (for example the first half of a postcode, not the whole thing).&lt;/p&gt;

&lt;p&gt;There are no address lines, no full postcodes, and no trade values or volumes. The rows say who traded under a code, not how much.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. From a product to a code
&lt;/h2&gt;

&lt;p&gt;HS codes classify goods. The first two digits give a chapter, four digits a heading, six digits a subheading. UK trade statistics use eight digits (CN8), which narrow the subheading further. The sample above uses the four-digit heading 8419.&lt;/p&gt;

&lt;p&gt;Pick the longest code you are sure of. A short code is broader, so it brings back more traders; a long one is narrower. If you search on a shorter code, traders whose rows carry a longer code beneath it are included. That is why each row lists its codes at several lengths (&lt;code&gt;hs4Codes&lt;/code&gt;, &lt;code&gt;hs6Codes&lt;/code&gt;, &lt;code&gt;cn8Codes&lt;/code&gt;): you can check which code a trader actually matched on.&lt;/p&gt;

&lt;p&gt;To find a code for a product, look it up in the UK Trade Tariff and note it at the length you can defend.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Running a search
&lt;/h2&gt;

&lt;p&gt;The input takes the commodity codes, whether you want imports, exports or both, and a month range. The quickest way to see it work is the example Task linked above, which is set up for HS 8419 imports and 20 rows. Run it, open the dataset, and you have the same kind of rows described here. The Kaggle sample has both flows; the example Task is imports only, so the two will not be identical.&lt;/p&gt;

&lt;p&gt;A run is charged at $0.005 plus $0.002 per trader row, so a 20-row search comes to about $0.045.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Reading the rows in Python
&lt;/h2&gt;

&lt;p&gt;Export the dataset as JSON from the Apify Console and load it. This snippet counts rows by flow, lists traders that appear in both flows, and shows the most common postcode districts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traders.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In both flows:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postcodeDistrict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A row looks like this (illustrative only: the name is an invented example and the code lists are shortened):&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Trading 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;"postcodeDistrict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EX2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"flow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"month"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hs6Codes"&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;"..."&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;Other fields include &lt;code&gt;traderId&lt;/code&gt;, &lt;code&gt;traderKey&lt;/code&gt;, &lt;code&gt;cn8Codes&lt;/code&gt;, &lt;code&gt;hs4Codes&lt;/code&gt;, &lt;code&gt;otherCodes&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;publishedAt&lt;/code&gt; and &lt;code&gt;matchType&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Looking up a named company, and watching a code
&lt;/h2&gt;

&lt;p&gt;The same Actor can look up a named company and can alert you to traders that are new to a code. The Actor page describes how to set either up.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Postcode district only: no address lines and no full postcodes.&lt;/li&gt;
&lt;li&gt;Sole traders are left out, but the name rule is not a guarantee. If you see a name that should not be there, or you want a business removed, email &lt;a href="mailto:info@yappman.com"&gt;info@yappman.com&lt;/a&gt; with the name as it appears in the data. I reply within one month.&lt;/li&gt;
&lt;li&gt;No trade values or volumes.&lt;/li&gt;
&lt;li&gt;HMRC publishes the lists monthly, so the data trails the month it describes. The Kaggle sample covers August 2026.&lt;/li&gt;
&lt;li&gt;Names and codes are as published; I do not correct them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Licence and credit
&lt;/h2&gt;

&lt;p&gt;The data is published under the Open Government Licence v3.0, which does not cover personal data; that is why names without an organisation marker are withheld. Keep this credit line with any copy or use:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Contains public sector information licensed under the Open Government Licence v3.0 (HMRC, UK Trade Info).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Compiled by Yappman Data. Not affiliated with HMRC.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Cost
&lt;/h2&gt;

&lt;p&gt;$0.005 a run plus $0.002 per trader row. The 20-row example costs about $0.045. Pay-per-event pricing means a larger search costs in proportion to the rows it returns.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Free 20-row sample (HS 8419, August 2026): &lt;a href="https://www.kaggle.com/datasets/russellyapp/uk-importers-and-exporters-hs-8419-aug-2026" rel="noopener noreferrer"&gt;https://www.kaggle.com/datasets/russellyapp/uk-importers-and-exporters-hs-8419-aug-2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example Task (HS 8419 imports, 20 rows): &lt;a href="https://apify.com/yappman/uk-importers-exporters/examples/uk-importers-of-heating-and-cooling-machinery" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-importers-exporters/examples/uk-importers-of-heating-and-cooling-machinery&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Actor: &lt;a href="https://apify.com/yappman/uk-importers-exporters" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-importers-exporters&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built the Actor and publish the sample. Corrections and questions: &lt;a href="mailto:info@yappman.com"&gt;info@yappman.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>python</category>
      <category>apify</category>
      <category>datascience</category>
    </item>
    <item>
      <title>After an Actor reboot, the Apify SDK forgets what it has already charged</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:11:15 +0000</pubDate>
      <link>https://dev.to/yappman/after-an-actor-reboot-the-apify-sdk-forgets-what-it-has-already-charged-5a09</link>
      <guid>https://dev.to/yappman/after-an-actor-reboot-the-apify-sdk-forgets-what-it-has-already-charged-5a09</guid>
      <description>&lt;p&gt;I build pay-per-event Actors for the Apify Store. This week I tested what happens to charging when an Actor reboots in the middle of a run, because Actors can be rebooted or migrated during long runs. I tested a forced reboot; I have not tested a platform migration. The result surprised me, so here it is, with the fix I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;I wrote a small private Actor that charges one event per dataset row, then forces a reboot part-way through with &lt;code&gt;Actor.reboot()&lt;/code&gt;. I ran it with a low "maximum cost per run" so the limit would matter, and checked the run metadata afterwards. I used Apify SDK for Python 4.0.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found
&lt;/h2&gt;

&lt;p&gt;After the reboot the SDK reported zero events charged, even though a few had already been charged before the reboot. As far as I can tell, the SDK takes its charged counts from environment variables set when the container starts, so a restarted container starts from the old numbers instead of the current ones.&lt;/p&gt;

&lt;p&gt;In one test, the run metadata ended up showing more charged events than the maximum I had set. Apify's documentation says the platform enforces the limit and that charges above the charged count are not billed, so I can't say a customer would have been billed for the extra events. What I can say is that the run's own record disagreed with the limit I had set, and my Actor had no way to notice. I'd rather not rely on the platform to quietly absorb that.&lt;/p&gt;

&lt;p&gt;I haven't seen an existing SDK issue describing this exact behaviour, so it may be worth a report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix I use
&lt;/h2&gt;

&lt;p&gt;Three small pieces, all inside the Actor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep your own record of charging.&lt;/strong&gt; In my Actors, a &lt;code&gt;CHARGE_STATE&lt;/code&gt; record in the run's key-value store is written as "pending" before each charge and updated to "charged" afterwards. After a restart you can read it and know what really happened, even if the SDK's counts say zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedupe against what you've already stored.&lt;/strong&gt; Before pushing a row, check whether it is already in the dataset. A restart then can't charge for the same row twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep your own spend limit.&lt;/strong&gt; Work out how many events you may still charge as: the user's maximum, minus the larger of your own record of earlier spend and the SDK's total, minus what you've charged since this start. Use &lt;code&gt;Decimal&lt;/code&gt; for the money and round down. Never use floats for prices: &lt;code&gt;(0.011 - 0.005) / 0.002&lt;/code&gt; gives 2.9999999999999996 in floats, so rounding down would lose an event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With those in place my test Actor stopped at exactly the limit I set ($0.011, three rows) in a run that rebooted, and a control run with a higher limit produced no duplicate rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own Actor
&lt;/h2&gt;

&lt;p&gt;Use a throwaway copy, a low per-run cost limit, and a forced &lt;code&gt;Actor.reboot()&lt;/code&gt; part-way through. Then read the run metadata and compare the charged counts with the maximum you set, and with the number of distinct rows you expect to have been charged. If they disagree, you have the same gap.&lt;/p&gt;

&lt;p&gt;I'm happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>python</category>
      <category>webscraping</category>
      <category>monetization</category>
    </item>
  </channel>
</rss>
