<?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: Apify</title>
    <description>The latest articles on DEV Community by Apify (apify).</description>
    <link>https://dev.to/apify</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%2Forganization%2Fprofile_image%2F2171%2F8c96d506-957a-4ad7-8e96-f083077b4d3f.png</url>
      <title>DEV Community: Apify</title>
      <link>https://dev.to/apify</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apify"/>
    <language>en</language>
    <item>
      <title>Investor prospecting lists from PitchBook and LinkedIn data</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:21:25 +0000</pubDate>
      <link>https://dev.to/apify/investor-prospecting-lists-from-pitchbook-and-linkedin-data-an1</link>
      <guid>https://dev.to/apify/investor-prospecting-lists-from-pitchbook-and-linkedin-data-an1</guid>
      <description>&lt;h2&gt;
  
  
  Investor prospecting lists from PitchBook and LinkedIn data
&lt;/h2&gt;

&lt;p&gt;Fundraising research usually means either paying for a PitchBook or Crunchbase seat the founder only needs for a few months, or manually building a spreadsheet from investor websites and LinkedIn one profile at a time. Neither is a great use of the limited time founders have during an active raise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Investor prospecting with public data&lt;/strong&gt; means joining public investor profile data with public LinkedIn partner profiles into one targeted, sector- and stage-matched list, instead of paying for a full research-platform seat or building the list one profile at a time by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use PitchBook Investors Scraper to collect public investor profiles — firm name, focus areas, and stated investment criteria — without a PitchBook subscription. Use LinkedIn Profile Scraper to enrich individual partner or associate profiles with role, background, and current firm. Combine both into a prospecting list segmented by sector and stage fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investor prospecting stack
&lt;/h2&gt;

&lt;p&gt;The stack uses two CrawlerBros Actors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/pitchbook-investors-scraper" rel="noopener noreferrer"&gt;PitchBook Investors Scraper&lt;/a&gt;&lt;/strong&gt; for public investor profiles without a subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/linkedin-profile-scraper" rel="noopener noreferrer"&gt;LinkedIn Profile Scraper&lt;/a&gt;&lt;/strong&gt; for public LinkedIn profile information without login.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fim6s6ee51b4a7zz2dnut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fim6s6ee51b4a7zz2dnut.png" alt="PitchBook Investors Scraper Actor page on Apify, showing profile URL input fields used for public investor profile collection" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Sector and stage criteria&lt;br&gt;
    -&amp;gt; PitchBook Investors Scraper (firm-level investor profiles)&lt;br&gt;
    -&amp;gt; LinkedIn Profile Scraper (partner-level enrichment)&lt;br&gt;
    -&amp;gt; segmented investor prospecting list&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Firm-level data answers "who invests in this"; partner-level data answers "who do I email."&lt;/strong&gt; Both are needed for outreach that isn't generic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start from known profile URLs, not broad category browsing.&lt;/strong&gt; Both Actors work best against specific, already-identified profile pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus fields go stale.&lt;/strong&gt; A firm's stated sector/stage focus can change; re-verify before outreach, not just before building the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fit beats volume.&lt;/strong&gt; A well-matched list of 20-50 firms consistently outperforms an unfiltered list of hundreds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;For PitchBook Investors Scraper, start from firm or investor profile URLs discovered through sector research rather than trying to enumerate an entire investor category in one run — public profile pages are the reliable input; broad category browsing is not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "profileUrls": [&lt;br&gt;
    "https://pitchbook.com/profiles/investor/example-1",&lt;br&gt;
    "https://pitchbook.com/profiles/investor/example-2"&lt;br&gt;
  ],&lt;br&gt;
  "includeInvestmentCriteria": true&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For LinkedIn Profile Scraper, feed it the specific partner or associate profile URLs found through the PitchBook data or firm websites, since public profile scraping works best against known URLs rather than broad search.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;The PitchBook row needs firm name, focus sectors, stage focus, and stated check size when available. The LinkedIn row needs name, title, current firm, and background summary. Joining the two gives a prospecting row that answers both "does this firm invest in what I'm building" and "who specifically should I reach out to."&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 1: targeted seed and Series A outreach
&lt;/h2&gt;

&lt;p&gt;A founder raising a specific round size in a specific sector can filter PitchBook profiles for matching stage and sector focus, then use LinkedIn enrichment to identify the right partner to reach rather than sending a cold email to a generic firm inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 2: warm-intro path mapping
&lt;/h2&gt;

&lt;p&gt;Once a target investor list exists, cross-referencing partner LinkedIn backgrounds against a founder's own network (previous companies, schools, mutual connections) helps prioritize which firms are reachable through a warm introduction versus cold outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 3: competitive fundraising intelligence
&lt;/h2&gt;

&lt;p&gt;Tracking which investors a competitor has raised from, where that's publicly known, helps a founder understand which firms are already active in the category and how to differentiate outreach messaging for firms likely to be comparing the two companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start from known profile URLs, not broad scraping.&lt;/strong&gt; Both Actors work best against specific, already-identified profile pages rather than attempting to enumerate an entire category of investors in one pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect what's public.&lt;/strong&gt; Only collect information visible on public profile pages. Avoid combining this with any data source that would require login credentials or private access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-verify stage and sector focus before outreach.&lt;/strong&gt; Investor focus areas change; a profile scraped months ago may not reflect a firm's current thesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prioritize fit over volume.&lt;/strong&gt; A list of 20 well-matched investors outperforms a list of 200 firms scraped without sector or stage filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firm-level data alone produces low response rates.&lt;/strong&gt; A firm-level list without partner-level context means outreach goes to generic inboxes. Adding LinkedIn partner enrichment makes outreach targeted enough to get meaningfully better response rates.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Data freshness&lt;/th&gt;
&lt;th&gt;Partner-level detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PitchBook/Crunchbase subscription&lt;/td&gt;
&lt;td&gt;Expensive, ongoing&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual research&lt;/td&gt;
&lt;td&gt;Free, very slow&lt;/td&gt;
&lt;td&gt;Depends on effort&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify PitchBook + LinkedIn pipeline&lt;/td&gt;
&lt;td&gt;Pay per run&lt;/td&gt;
&lt;td&gt;On demand&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current Pricing tab on each Actor page before running at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify replace a PitchBook subscription?
&lt;/h2&gt;

&lt;p&gt;It can replace the workflow of researching public investor profiles for a specific list; it doesn't replicate PitchBook's full paid database and analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is scraping public LinkedIn profiles allowed?
&lt;/h2&gt;

&lt;p&gt;LinkedIn Profile Scraper collects publicly visible profile information without login; always review current platform terms before large-scale collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  How big should a first investor list be?
&lt;/h2&gt;

&lt;p&gt;Most founders get better results from a well-matched list of 20-50 firms than an unfiltered list of hundreds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick 10 investor firms whose public focus matches your sector and stage, then run PitchBook Investors Scraper and LinkedIn Profile Scraper against them.&lt;/p&gt;

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

&lt;p&gt;Pick 10 investor firms whose public focus matches your sector and stage. Run &lt;a href="https://apify.com/crawlerbros/pitchbook-investors-scraper" rel="noopener noreferrer"&gt;PitchBook Investors Scraper&lt;/a&gt; against their profile pages, then use &lt;a href="https://apify.com/crawlerbros/linkedin-profile-scraper" rel="noopener noreferrer"&gt;LinkedIn Profile Scraper&lt;/a&gt; to find the right partner at each. A short, well-matched list beats a long, generic one.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>startup</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Facebook Marketplace intelligence: resale pricing and comment sentiment</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Wed, 16 Sep 2026 05:15:01 +0000</pubDate>
      <link>https://dev.to/apify/facebook-marketplace-intelligence-resale-pricing-and-comment-sentiment-4pdk</link>
      <guid>https://dev.to/apify/facebook-marketplace-intelligence-resale-pricing-and-comment-sentiment-4pdk</guid>
      <description>&lt;h2&gt;
  
  
  Facebook Marketplace intelligence: resale pricing and comment sentiment
&lt;/h2&gt;

&lt;p&gt;Facebook Marketplace is one of the largest resale and local-commerce platforms, and one of the least monitored. Most price-intelligence tools skip it entirely because it's local, listing-heavy, and constantly turning over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facebook Marketplace intelligence&lt;/strong&gt; means combining public listing data with public comment data to price resale inventory, spot unauthorized brand resale, and read how a community actually reacts to a local post — instead of relying on manual browsing or reaction-emoji counts alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use Facebook Marketplace Scraper to collect listing prices, condition, and seller data for a product category or location. Use Facebook Comments Scraper to pull public reactions on posts, whether that's a Marketplace listing, a local business page post, or a community group thread. Combine both to price resale inventory and read local sentiment in the same workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Marketplace intelligence stack
&lt;/h2&gt;

&lt;p&gt;The stack uses two CrawlerBros Actors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/facebook-marketplace-scraper" rel="noopener noreferrer"&gt;Facebook Marketplace Scraper&lt;/a&gt;&lt;/strong&gt; for listing data, pricing, and seller information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/facebook-comments-scraper" rel="noopener noreferrer"&gt;Facebook Comments Scraper&lt;/a&gt;&lt;/strong&gt; for public comments on posts and videos with metadata.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmos4fihe565xmjd3abve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmos4fihe565xmjd3abve.png" alt="Facebook Marketplace Scraper Actor page on Apify, showing category, location, and result-limit input fields used for resale price tracking" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Product category or location&lt;br&gt;
    -&amp;gt; Facebook Marketplace Scraper (listing prices, condition, seller)&lt;br&gt;
    -&amp;gt; Facebook Comments Scraper (reactions on flagged listings or posts)&lt;br&gt;
    -&amp;gt; resale price and sentiment dataset&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location beats keyword.&lt;/strong&gt; Marketplace is a local marketplace by design — a keyword search without a location filter returns inconsistent results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition explains price, not irrationality.&lt;/strong&gt; Two identical-looking listings at different prices are usually explained by condition, not by one seller being unreasonable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listing velocity beats a single price.&lt;/strong&gt; A category with fast-turning listings at a given price point signals real demand better than a handful of stale unsold listings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Public" has a specific meaning here.&lt;/strong&gt; Marketplace listings and public post comments qualify; private group content doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;For Facebook Marketplace Scraper, scope by category and location rather than a broad keyword search — Marketplace results are heavily location-weighted, so a keyword search without a location filter returns inconsistent results.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "category": "furniture",&lt;br&gt;
  "location": "Austin, TX",&lt;br&gt;
  "maxItems": 150,&lt;br&gt;
  "sortBy": "date_listed"&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Facebook Comments Scraper, feed it the specific post or listing URLs you want reactions on rather than trying to crawl comments broadly — Facebook's comment threading and pagination behave differently across post types, so targeted URL lists produce more reliable results than open-ended searches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;The Marketplace row needs title, price, condition, location, seller name, listing date, and listing URL. Price and condition together are what make resale comps useful — a "good condition" $150 listing and a "like new" $150 listing aren't the same data point.&lt;/p&gt;

&lt;p&gt;The Comments row needs comment text, author, timestamp, and post URL, so a spike in complaints or praise can be traced back to the specific listing or post that triggered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 1: resale price benchmarking
&lt;/h2&gt;

&lt;p&gt;A reseller flipping furniture, electronics, or collectibles can pull active listings for a category and location, then price new inventory against what's actually moving rather than guessing from a handful of manually checked listings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 2: gray-market and unauthorized resale monitoring
&lt;/h2&gt;

&lt;p&gt;A brand that sells through authorized retailers can search Marketplace for its product names to spot bulk resale, counterfeit listings, or unauthorized liquidation stock showing up at prices well below MSRP — a pattern that's hard to catch by browsing manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 3: local business post sentiment
&lt;/h2&gt;

&lt;p&gt;A local business posting about a new menu item, event, or policy change can pull comments on that post to see the actual reaction, rather than relying on the reaction-emoji count alone. Comment text often reveals &lt;em&gt;why&lt;/em&gt; a post landed well or badly, which the emoji summary doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scope by location first.&lt;/strong&gt; Marketplace is a local marketplace by design — location filtering matters more here than on almost any other platform in this batch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track condition alongside price.&lt;/strong&gt; Two identical-looking listings at different prices are often explained by condition, not by one seller being irrational.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch listing velocity, not just price.&lt;/strong&gt; A category with fast-turning listings at a given price point tells you more about real demand than a handful of stale listings sitting unsold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect what "public" means here.&lt;/strong&gt; Only collect what's visible without logging in or joining a private group. Marketplace listings and public post comments qualify; private group content does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price without condition confuses more than it clarifies.&lt;/strong&gt; Price without condition and listing age produces comps that look contradictory. Adding condition and days-listed as required fields fixes most of the confusion.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Local granularity&lt;/th&gt;
&lt;th&gt;Sentiment data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual browsing&lt;/td&gt;
&lt;td&gt;One category at a time&lt;/td&gt;
&lt;td&gt;High, but slow&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General resale-price tools&lt;/td&gt;
&lt;td&gt;Limited or no Marketplace coverage&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify Marketplace + Comments pipeline&lt;/td&gt;
&lt;td&gt;Category and location scoped&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current Pricing tab on each Actor page before running at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify scrape Facebook Marketplace listings?
&lt;/h2&gt;

&lt;p&gt;Yes. Facebook Marketplace Scraper extracts public listing data including price, condition, and seller information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I pull comments from any Facebook post?
&lt;/h2&gt;

&lt;p&gt;Facebook Comments Scraper works on public posts and videos; private group or profile content isn't accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this useful outside of reselling?
&lt;/h2&gt;

&lt;p&gt;Yes — brand protection, local sentiment tracking, and community research all use the same two-Actor pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick one product category and city, run Facebook Marketplace Scraper for current listings, then run Facebook Comments Scraper on the most-commented listings.&lt;/p&gt;

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

&lt;p&gt;Pick one product category and one city. Run &lt;a href="https://apify.com/crawlerbros/facebook-marketplace-scraper" rel="noopener noreferrer"&gt;Facebook Marketplace Scraper&lt;/a&gt; for current listings, then run &lt;a href="https://apify.com/crawlerbros/facebook-comments-scraper" rel="noopener noreferrer"&gt;Facebook Comments Scraper&lt;/a&gt; on the two or three most-commented listings you find. The price spread plus the comment tone will tell you more about that local market than either dataset alone.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>marketing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Finding flight price anomalies with Skiplagged data on Apify</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Tue, 15 Sep 2026 04:41:46 +0000</pubDate>
      <link>https://dev.to/apify/finding-flight-price-anomalies-with-skiplagged-data-on-apify-12gi</link>
      <guid>https://dev.to/apify/finding-flight-price-anomalies-with-skiplagged-data-on-apify-12gi</guid>
      <description>&lt;h2&gt;
  
  
  Finding flight price anomalies with Skiplagged data on Apify
&lt;/h2&gt;

&lt;p&gt;Flight pricing is famously inconsistent — the same route can price differently depending on the exact combination of origin, destination, and connection chosen, sometimes by a wide margin for what's effectively the same trip. Skiplagged built a business around surfacing exactly these gaps by comparing direct fares against fares for longer itineraries that happen to pass through the same destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flight price arbitrage detection&lt;/strong&gt; means systematically comparing fares across itinerary structures for the same route and destination, instead of checking prices manually route by route. For travel content sites, deal-alert businesses, and corporate travel teams watching cost, that pricing behavior is worth monitoring on a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use Skiplagged Scraper to collect flight search results for specific routes and date ranges, including price, airline, and itinerary structure. Run it across a route set on a schedule and flag results where a longer or connecting itinerary prices meaningfully below the direct fare for the same destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flight arbitrage setup
&lt;/h2&gt;

&lt;p&gt;The setup uses one CrawlerBros Actor:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/skiplagged-scraper" rel="noopener noreferrer"&gt;Skiplagged Flight Search Scraper&lt;/a&gt;&lt;/strong&gt; for flight search results with prices, airlines, and booking data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28lxr66ye16znpwlb3oq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28lxr66ye16znpwlb3oq.png" alt="Skiplagged Flight Search Scraper Actor page on Apify, showing origin, destination, and departure date range input fields used for fare comparison" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The monitoring loop:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Route and date list&lt;br&gt;
    -&amp;gt; Skiplagged Flight Search Scraper&lt;br&gt;
    -&amp;gt; price-by-itinerary dataset&lt;br&gt;
    -&amp;gt; anomaly flagging (direct vs. connecting fare gaps)&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The gap is the signal, not the price.&lt;/strong&gt; The interesting data point is the difference between itinerary types for the same destination, not any single fare in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single-date search misses most gaps.&lt;/strong&gt; Fare anomalies often appear on specific days of the week; search across a date range, not one fixed date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fares move constantly.&lt;/strong&gt; A one-time snapshot only shows one moment in a moving market — schedule the run instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some routes show gaps consistently; most don't.&lt;/strong&gt; A shortlist of routes worth monitoring closely beats tracking every possible route equally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;Search by origin, destination, and date range rather than a single fixed date — fare anomalies often appear on specific days of the week or specific date combinations, and a single-date search will miss most of them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "origin": "JFK",&lt;br&gt;
  "destination": "LAX",&lt;br&gt;
  "departureDateRange": ["2026-11-01", "2026-11-30"],&lt;br&gt;
  "maxResults": 100&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Run the same route across a rolling date window on a schedule rather than a one-time pull — fare gaps shift as airline pricing algorithms react to booking activity, so a single snapshot only shows one moment in a constantly moving market.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;Each result row should carry origin, destination, departure date, price, airline, and itinerary type (direct vs. connecting, and whether the booked destination differs from the traveler's actual destination). The itinerary-type field is what turns a plain price list into an arbitrage-detection dataset — without it, a cheap connecting flight and a cheap direct flight look identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 1: travel deal content
&lt;/h2&gt;

&lt;p&gt;A travel deals site or newsletter can run a set of popular routes on a schedule and automatically flag the biggest fare gaps each week, turning a manual research task into a structured feed worth publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 2: corporate travel cost monitoring
&lt;/h2&gt;

&lt;p&gt;A company managing frequent travel on a fixed set of routes can track whether cheaper itinerary structures are consistently available on routes employees fly often, informing internal travel policy without requiring manual research for every trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 3: route-level pricing trend research
&lt;/h2&gt;

&lt;p&gt;Tracking the same route over months shows whether fare volatility on that route is increasing or decreasing, which is useful context for anyone deciding when to book rather than just what to book.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Track itinerary structure, not just price.&lt;/strong&gt; The interesting signal here is the &lt;em&gt;gap&lt;/em&gt; between itinerary types for the same destination, not the raw price of any single result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run on a rolling schedule.&lt;/strong&gt; Fares change constantly; a static dataset goes stale within days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be explicit about what "cheaper" means to your audience.&lt;/strong&gt; Booking a longer or connecting itinerary to save money involves trade-offs (no checked bags on the unused leg, itinerary risk) that are worth stating clearly in any published content built from this data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch for route-specific patterns.&lt;/strong&gt; Some routes show fare gaps consistently; others rarely do. Building a shortlist of routes worth monitoring closely is more efficient than tracking every possible route equally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price alone doesn't flag anomalies.&lt;/strong&gt; Without itinerary-type context, "found a cheap flight" alerts don't distinguish between a genuinely unusual gap and an ordinary low-season fare. Itinerary-type as a required field makes anomaly detection meaningful.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Update frequency&lt;/th&gt;
&lt;th&gt;Anomaly detection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual fare checking&lt;/td&gt;
&lt;td&gt;One route at a time&lt;/td&gt;
&lt;td&gt;Whenever you check&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight price alert apps&lt;/td&gt;
&lt;td&gt;Broad, but black-box logic&lt;/td&gt;
&lt;td&gt;Automated&lt;/td&gt;
&lt;td&gt;Limited transparency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify Skiplagged pipeline&lt;/td&gt;
&lt;td&gt;Custom route set&lt;/td&gt;
&lt;td&gt;Scheduled&lt;/td&gt;
&lt;td&gt;Transparent, itinerary-level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current Pricing tab on the Actor page before running at scale — cost depends on route count and date-range breadth.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify track flight price anomalies?
&lt;/h2&gt;

&lt;p&gt;Yes. Skiplagged Flight Search Scraper collects fare data across itinerary types for a given route and date range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this only useful for hidden-city ticketing?
&lt;/h2&gt;

&lt;p&gt;No — the underlying data is useful for any fare-comparison or travel-cost-monitoring use case, independent of any specific booking strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often should routes be re-scraped?
&lt;/h2&gt;

&lt;p&gt;Daily or every few days for routes you're actively monitoring; fare gaps can appear and disappear within a short window.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick one popular route and a 30-day date window, run Skiplagged Flight Search Scraper across it, then sort by the direct-vs-connecting price gap.&lt;/p&gt;

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

&lt;p&gt;Pick one popular route and a 30-day date window. Run &lt;a href="https://apify.com/crawlerbros/skiplagged-scraper" rel="noopener noreferrer"&gt;Skiplagged Flight Search Scraper&lt;/a&gt; across that window, then sort by the gap between direct and connecting itinerary pricing. The largest gaps are your first candidates worth digging into further.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>travel</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Giving AI agents live European fuel prices with Apify MCP</title>
      <dc:creator>Viktor Kondas</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:45:05 +0000</pubDate>
      <link>https://dev.to/apify/giving-ai-agents-live-european-fuel-prices-with-apify-mcp-10gf</link>
      <guid>https://dev.to/apify/giving-ai-agents-live-european-fuel-prices-with-apify-mcp-10gf</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;Giving AI agents live European fuel prices with Apify MCP&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;How I turned a CEE + DACH fuel-price Actor into a tool Cursor can call — and what I had to change once a model, not a spreadsheet, was reading the output.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built European Fuel Station Prices API (CEE + DACH) (&lt;a href="https://apify.com/kondasviktor/cee-dach-fuel-intelligence" rel="noopener noreferrer"&gt;https://apify.com/kondasviktor/cee-dach-fuel-intelligence&lt;/a&gt;) as a normal Apify Actor first: normalized station prices across Central Europe and DACH, scheduled monitoring, and a route cheapest-stop mode. Then I wired the same Actor into Cursor through the Apify MCP server (&lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;https://docs.apify.com/platform/integrations/mcp&lt;/a&gt;). This piece is about that second step — Theme 2 in Apify's Content Program: your Actor as a tool for AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What you need&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A free Apify (&lt;a href="https://apify.com" rel="noopener noreferrer"&gt;https://apify.com&lt;/a&gt;) account
&lt;/li&gt;
&lt;li&gt;Cursor (&lt;a href="https://cursor.com" rel="noopener noreferrer"&gt;https://cursor.com&lt;/a&gt;) (or another MCP-capable client)
&lt;/li&gt;
&lt;li&gt;Willingness to let the agent call cloud Actors (each run bills PPE; Austria smoke tests land on the $0.10 minimum)
&lt;/li&gt;
&lt;li&gt;Optional: a Fuelo (&lt;a href="https://fuelo.net/about/api%5C_key%5C_request" rel="noopener noreferrer"&gt;https://fuelo.net/about/api\_key\_request&lt;/a&gt;) API key if you want HU/PL/CZ/SK/CH/BG coverage (BYOK — the Actor never uses platform keys)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No local scrape setup. The Actor already runs on Apify; MCP is how the agent reaches it.&lt;/p&gt;

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

&lt;p&gt;Ask any LLM-based agent &lt;em&gt;"where should I fill up between Budapest and Vienna?"&lt;/em&gt; and you get a fluent answer built from training data that is months or years stale. Pump prices move daily. Logistics teams want live, per-station, cross-border numbers — not a national average from memory.&lt;/p&gt;

&lt;p&gt;CEE adds a second failure mode. Slovakia can apply dual pricing for foreign-plated vehicles (dualPricingForForeignPlates: true, verified 2026-07-24 in my Actor's regulatory config). Croatia can run price caps on regular stations while highway stations differ (priceCapActive: true, same date). Austria restricts when prices may increase to Mon/Wed/Fri (priceIncreaseRestrictedDays: ["MO","WE","FR"]). None of that belongs in model weights. It belongs in structured fields an agent reads at call time.&lt;/p&gt;

&lt;p&gt;That gap — volatile, jurisdiction-specific data an LLM cannot reliably know — is what MCP tools are for. The interesting engineering is not scraping a price board. It is designing output so an agent knows how much to trust each number and receives few enough tokens to reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why I built a regional Actor in Cursor&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I live in CEE. The Apify Store had US/FR/AU fuel Actors; station-level CEE + DACH coverage did not. I wanted one schema across DE, AT, CH, HU, PL, CZ, SK, RO, HR, SI, and BG — official feeds where they exist, licensed aggregators elsewhere, EUR-normalized for cross-border comparison.&lt;/p&gt;

&lt;p&gt;The Actor is live on Store (build 0.1.10 at time of writing), pay-per-event (&lt;a href="https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event" rel="noopener noreferrer"&gt;https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event&lt;/a&gt;), with public example tasks. No buyer key for AT, SI, HR, RO. BYOK: Tankerkönig for DE, Fuelo for CH/HU/PL/CZ/SK/BG. You can smoke-test Austria in under a minute for the $0.10 minimum run charge.&lt;/p&gt;

&lt;p&gt;I built the Actor in Cursor — the same client that now calls it via MCP. That closed loop mattered: every schema tweak got exercised by an agent that actually had to answer a logistics question, not by me staring at a dataset table.&lt;/p&gt;

&lt;p&gt;Data sources are official public APIs or licensed aggregators with BYOK. The Actor does not scrape pump websites behind a login or ignore site terms; for Poland and similar Fuelo countries, missing a key fails loudly instead of inventing prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How the Actor is built&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each country is a small adapter behind one interface. Austria reads E-Control's public JSON API, maps DIE/SUP/GAS into canonical diesel/petrol95/cng, and tags every row dataConfidence: OFFICIAL_REALTIME with an honest sourceMessage that E-Control returns cheapest-N, not the full market:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (typescript)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;// src/adapters/at.ts (excerpt)&lt;br&gt;&lt;br&gt;
url.searchParams.set('fuelType', mapFuel(query.fuel));&lt;br&gt;&lt;br&gt;
// ...&lt;br&gt;&lt;br&gt;
return {&lt;br&gt;&lt;br&gt;
  stationId: s.id != null ? String(s.id) : null,&lt;br&gt;&lt;br&gt;
  country: 'AT',&lt;br&gt;&lt;br&gt;
  diesel: typeof die === 'number' ? die : null,&lt;br&gt;&lt;br&gt;
  petrol95: typeof sup === 'number' ? sup : null,&lt;br&gt;&lt;br&gt;
  dataConfidence: 'OFFICIAL_REALTIME',&lt;br&gt;&lt;br&gt;
  source: 'E-Control Spritpreisrechner',&lt;br&gt;&lt;br&gt;
  sourceMessage:&lt;br&gt;&lt;br&gt;
    'E-Control returns cheapest-N stations for the search — not every station nationwide',&lt;br&gt;&lt;br&gt;
  regulatoryFlags: regulatoryFlagsFor('AT'),&lt;br&gt;&lt;br&gt;
};&lt;br&gt;&lt;br&gt;
Prices pass through ECB FX into *Eur and priceEur so a Budapest→Vienna comparison is not HUF guesswork against EUR memory.&lt;/p&gt;

&lt;p&gt;Route mode keeps cheapest-stop math deterministic — haversine detour, not LLM arithmetic:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (typescript)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;// src/route/geometry.ts (excerpt)&lt;br&gt;&lt;br&gt;
const detour = toStation + stationToDest - direct;&lt;br&gt;&lt;br&gt;
if (detour &amp;gt; opts.maxDetourKm) continue;&lt;br&gt;&lt;br&gt;
if (!best || price &amp;lt; best.price || (price === best.price &amp;amp;&amp;amp; detour &amp;lt; best.detour)) {&lt;br&gt;&lt;br&gt;
  best = { station: s, price, detour, fromOrigin: toStation };&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
Partial runs never invent rows. markCountryStatus classifies each country (ok, empty, missing_fuelo_key, …) and the run still succeeds for the rest:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (typescript)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;// src/main.ts (excerpt)&lt;br&gt;&lt;br&gt;
if (errMsg) {&lt;br&gt;&lt;br&gt;
  countryStatus[country] = classifyAdapterError(errMsg).status;&lt;br&gt;&lt;br&gt;
  return;&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
Pricing is explicit PPE: station-price $0.0025, route-recommendation $0.05, optional ai-digest $0.15 (BYOK, off by default), $0.10 minimum per run.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Architecture: agent decides, Actor evidences&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Code (text)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;User question&lt;br&gt;&lt;br&gt;
    → Cursor agent&lt;br&gt;&lt;br&gt;
    → Apify MCP (call-actor / fetch-actor-details)&lt;br&gt;&lt;br&gt;
    → Fuel Intelligence Actor&lt;br&gt;&lt;br&gt;
    → country adapters → live feeds&lt;br&gt;&lt;br&gt;
    → normalized dataset&lt;br&gt;&lt;br&gt;
    → agent recommendation&lt;br&gt;&lt;br&gt;
The agent is the decision layer. The Actor is the evidence layer. MCP is the bridge that removes the manual Console handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Wiring Cursor to Apify MCP&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Apify hosts MCP at &lt;a href="https://mcp.apify.com" rel="noopener noreferrer"&gt;https://mcp.apify.com&lt;/a&gt; with OAuth on first connect — no API token in your config file. In .cursor/mcp.json:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "mcpServers": {&lt;br&gt;&lt;br&gt;
    "apify": {&lt;br&gt;&lt;br&gt;
      "url": "&lt;a href="https://mcp.apify.com/?tools=fetch-actor-details,kondasviktor/cee-dach-fuel-intelligence" rel="noopener noreferrer"&gt;https://mcp.apify.com/?tools=fetch-actor-details,kondasviktor/cee-dach-fuel-intelligence&lt;/a&gt;"&lt;br&gt;&lt;br&gt;
    }&lt;br&gt;&lt;br&gt;
  }&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
That pin matches the snippet on the Actor's Store page: fetch-actor-details plus the Actor id. For day-to-day exploration I also use a broader pin (actors,docs,...) so the agent can search Store and read docs; for a fleet assistant that always calls this Actor, the narrow pin skips a search round trip.&lt;/p&gt;

&lt;p&gt;Reload Cursor, sign in to Apify when prompted, then enable the server under &lt;strong&gt;Settings → Tools &amp;amp; MCP&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Cursor MCP configuration in mcp.json showing the Apify server URL with the cee-dach-fuel-intelligence Actor pinned as a tool&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Cursor MCP tool list for the Apify server — all tools enabled including kondasviktor/cee-dach-fuel-intelligence&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Typical discovery path: search-actors → fetch-actor-details (read the input schema (&lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema)" rel="noopener noreferrer"&gt;https://docs.apify.com/platform/actors/development/actor-definition/input-schema)&lt;/a&gt;) → call-actor. The input schema tells the agent required fields (mode, lat/lng for point, origin/destination for route). Apify can also infer output shapes from recent successful runs; my Actor's dataset schema documents row types (station-price, route-recommendation, run-status). The Actor output schema in Console is mainly deep links (dataset, runOverview) — field typing for agents comes from input + dataset schemas, not from those links alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Three real agent runs (August 2026)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I did not tell the agent &lt;em&gt;"run my fuel Actor."&lt;/em&gt; I asked logistics questions and let it resolve the tool through MCP. All runs below are cloud runs via Apify MCP; run IDs are real. Origin in Console shows &lt;strong&gt;MCP&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;A — Vienna petrol, key-free official data&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; &lt;em&gt;I need current petrol prices near central Vienna for a delivery van, and tell me how reliable that data is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input the agent constructed:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "mode": "point",&lt;br&gt;&lt;br&gt;
  "countries": ["AT"],&lt;br&gt;&lt;br&gt;
  "lat": 48.2082,&lt;br&gt;&lt;br&gt;
  "lng": 16.3738,&lt;br&gt;&lt;br&gt;
  "radiusKm": 8,&lt;br&gt;&lt;br&gt;
  "fuel": "petrol95",&lt;br&gt;&lt;br&gt;
  "maxStations": 8,&lt;br&gt;&lt;br&gt;
  "enableAIDigest": false&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Run:&lt;/strong&gt; chJk2dCvN45DnWGKC · SUCCEEDED in 4.5s · 10 dataset items · countryStatus.AT: ok&lt;/p&gt;

&lt;p&gt;Cheapest row returned:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "recordType": "station-price",&lt;br&gt;&lt;br&gt;
  "stationId": "1494440",&lt;br&gt;&lt;br&gt;
  "brand": "TMC",&lt;br&gt;&lt;br&gt;
  "name": "TMC Werkstatt &amp;amp; Tankstelle",&lt;br&gt;&lt;br&gt;
  "country": "AT",&lt;br&gt;&lt;br&gt;
  "city": "Wien",&lt;br&gt;&lt;br&gt;
  "petrol95": 1.669,&lt;br&gt;&lt;br&gt;
  "petrol95Eur": 1.669,&lt;br&gt;&lt;br&gt;
  "priceEur": 1.669,&lt;br&gt;&lt;br&gt;
  "currency": "EUR",&lt;br&gt;&lt;br&gt;
  "dataConfidence": "OFFICIAL_REALTIME",&lt;br&gt;&lt;br&gt;
  "source": "E-Control Spritpreisrechner",&lt;br&gt;&lt;br&gt;
  "sourceStatus": "OK"&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Agent conclusion:&lt;/strong&gt; TMC at €1.669 is the cheapest nearby petrol95; data is official E-Control realtime, but cheapest-N — not every Austrian station.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Scenario A run detail in Apify Console — run chJk2dCvN45DnWGKC, SUCCEEDED, 10 results, origin MCP&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Scenario A dataset — Vienna petrol95 station-price rows sorted by price, TMC first at €1.669, all with OFFICIAL_REALTIME confidence&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;B — Budapest→Vienna diesel route&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; &lt;em&gt;I'm driving a diesel van from Budapest to Vienna today. Where should I stop to fill up without a big detour, and how sure are you about that price?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input (Fuelo key redacted — buyers supply their own):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "mode": "route",&lt;br&gt;&lt;br&gt;
  "countries": ["HU", "AT"],&lt;br&gt;&lt;br&gt;
  "originLat": 47.4979,&lt;br&gt;&lt;br&gt;
  "originLng": 19.0402,&lt;br&gt;&lt;br&gt;
  "destinationLat": 48.2082,&lt;br&gt;&lt;br&gt;
  "destinationLng": 16.3738,&lt;br&gt;&lt;br&gt;
  "fuel": "diesel",&lt;br&gt;&lt;br&gt;
  "maxDetourKm": 20,&lt;br&gt;&lt;br&gt;
  "maxStations": 25,&lt;br&gt;&lt;br&gt;
  "radiusKm": 15,&lt;br&gt;&lt;br&gt;
  "enableAIDigest": false,&lt;br&gt;&lt;br&gt;
  "fueloApiKey": "YOUR_FUELO_KEY"&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Run:&lt;/strong&gt; mT6vN8TOsCG7P5d6s · SUCCEEDED in 12.9s · 39 items · one route-recommendation row&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "recordType": "route-recommendation",&lt;br&gt;&lt;br&gt;
  "stationId": "fuelo:HU:36084",&lt;br&gt;&lt;br&gt;
  "brand": "Slovnaft",&lt;br&gt;&lt;br&gt;
  "country": "HU",&lt;br&gt;&lt;br&gt;
  "city": "Bratislava",&lt;br&gt;&lt;br&gt;
  "fuel": "diesel",&lt;br&gt;&lt;br&gt;
  "price": 1.706,&lt;br&gt;&lt;br&gt;
  "priceEur": 1.706,&lt;br&gt;&lt;br&gt;
  "currency": "EUR",&lt;br&gt;&lt;br&gt;
  "distanceFromOriginKm": 158.38,&lt;br&gt;&lt;br&gt;
  "estimatedDetourKm": 1.03,&lt;br&gt;&lt;br&gt;
  "dataConfidence": "COMMUNITY_AGGREGATOR_UNVERIFIED"&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Agent conclusion:&lt;/strong&gt; Cheapest stop within ~1 km detour is Slovnaft diesel at €1.706 — but the agent must hedge: COMMUNITY_AGGREGATOR_UNVERIFIED, not official regulator data. The same run also returned OFFICIAL_REALTIME E-Control rows for AT segments; mixing tiers in one answer without naming confidence would be wrong.&lt;/p&gt;

&lt;p&gt;This is token economics in practice: route mode returns one recommendation record plus bounded station rows (maxStations), not an unbounded national dump. MCP still returns full field lists per row — context control is the Actor's modes and caps, not automatic truncation in MCP.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Scenario B dataset — Budapest-Vienna route, row 42 is the route-recommendation: Slovnaft Bratislava, diesel €1.706, ~1.03 km detour&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;C — Poland without a Fuelo key (honest failure)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; &lt;em&gt;Current diesel near central Warsaw — same kind of answer as Vienna.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt; point query for PL — fueloApiKey deliberately omitted even though I have a key locally.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "mode": "point",&lt;br&gt;&lt;br&gt;
  "countries": ["PL"],&lt;br&gt;&lt;br&gt;
  "lat": 52.2297,&lt;br&gt;&lt;br&gt;
  "lng": 21.0122,&lt;br&gt;&lt;br&gt;
  "radiusKm": 10,&lt;br&gt;&lt;br&gt;
  "fuel": "diesel",&lt;br&gt;&lt;br&gt;
  "maxStations": 10,&lt;br&gt;&lt;br&gt;
  "enableAIDigest": false&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Run:&lt;/strong&gt; RJCT720eHg3INuOzg · SUCCEEDED in 3.6s · 0 stations · 1 run-status row&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code (json)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;&lt;br&gt;
  "recordType": "run-status",&lt;br&gt;&lt;br&gt;
  "countryStatus": { "PL": "missing_fuelo_key" },&lt;br&gt;&lt;br&gt;
  "stationCount": 0,&lt;br&gt;&lt;br&gt;
  "okStationCount": 0,&lt;br&gt;&lt;br&gt;
  "notes": [&lt;br&gt;&lt;br&gt;
    "PL: PL uses Fuelo.net (BYOK). Set input fueloApiKey from &lt;a href="https://fuelo.net/about/api%5C_key%5C_request" rel="noopener noreferrer"&gt;https://fuelo.net/about/api\_key\_request&lt;/a&gt; — we never use platform keys."&lt;br&gt;&lt;br&gt;
  ]&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Agent conclusion:&lt;/strong&gt; Polish station data is unavailable until the caller supplies a Fuelo key — do not invent Warsaw diesel prices. Honest failure beats silent emptiness: the run succeeded, the status says &lt;em&gt;why&lt;/em&gt; it is empty.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Scenario C run detail — run RJCT720eHg3INuOzg, SUCCEEDED with 1 result, origin MCP, completed in 3 seconds&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Scenario C Actor log — ERROR line shows PL uses Fuelo.net BYOK, followed by INFO: stations=0, status=missing_fuelo_key&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What MCP removed&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Without MCP, a human (or agent pretending to be one) does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Apify Console
&lt;/li&gt;
&lt;li&gt;Find the Actor
&lt;/li&gt;
&lt;li&gt;Fill input JSON
&lt;/li&gt;
&lt;li&gt;Start run
&lt;/li&gt;
&lt;li&gt;Wait
&lt;/li&gt;
&lt;li&gt;Open dataset
&lt;/li&gt;
&lt;li&gt;Export or copy rows
&lt;/li&gt;
&lt;li&gt;Paste into chat
&lt;/li&gt;
&lt;li&gt;Ask the model to recommend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With MCP, steps 2–8 collapse into one agent turn: recognize live fuel data is needed → fetch-actor-details → call-actor → reason on structured rows. The manual handoff disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Design choices that mattered for agents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A few patterns I would steal for any live-data Actor meant for MCP:&lt;/p&gt;

&lt;p&gt;Put provenance on the row, not only in the README. dataConfidence, source, and sourceMessage ride on every station row so the agent can hedge inside one tool call. A spreadsheet buyer scrolls to methodology; an agent cannot.&lt;/p&gt;

&lt;p&gt;Keep regulation in dated config, not model memory. regulatoryFlags come from JSON (lastVerified: 2026-07-24). The optional ai-digest PPE event stays off the hot path — narrative over aggregates is for scheduled batch runs, not every "where's cheapest" query.&lt;/p&gt;

&lt;p&gt;Keep the core deterministic. Currency normalization, detour math, and cheapest-stop selection stay in TypeScript. Let the agent interpret; do not let it calculate pump prices.&lt;/p&gt;

&lt;p&gt;Name fields so a model can infer meaning. estimatedDetourKm and missing_fuelo_key beat internal enums like tier2.&lt;/p&gt;

&lt;p&gt;Design for agent call patterns. One route call beats eleven country calls. Cap maxStations. Batch countries in a single input when the schema allows it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What broke while building this&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AT always needs coordinates. Even country mode feeds a national centroid into E-Control's lat/lng search — there is no "whole country" dump.
&lt;/li&gt;
&lt;li&gt;asOfDate on E-Control throws explicitly when asOfDateStrict=true instead of silently serving live prices as history.
&lt;/li&gt;
&lt;li&gt;Tankerkönig free tier caps radius at 25 km — the input schema enforces the same cap.
&lt;/li&gt;
&lt;li&gt;Missing BYOK must not look like "no stations exist." Poland without fueloApiKey → missing_fuelo_key, not an empty success story.
&lt;/li&gt;
&lt;li&gt;Early MCP pins that only exposed call-actor without fetch-actor-details made the agent guess required fields. Pinning the Actor &lt;em&gt;and&lt;/em&gt; details fixed that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I would do differently&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I would ship the dataset schema and dataConfidence enums before the first public Store listing. I added provenance after watching an agent treat a Fuelo community price like E-Control realtime. I would also document MCP pin recipes on the Store page earlier — the narrow pin vs broad actors,docs pin is not obvious from Console alone.&lt;/p&gt;

&lt;p&gt;Next on my list: richer route geometry than haversine (road network detours), and clearer city/country consistency checks when a community feed returns odd location labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Takeaways&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One Actor now serves Console, API, schedules, automation tools — and agents via MCP. Fuel was the forcing function because bad answers are costly; the pattern generalizes to any live, messy, geographic dataset (EV charging, truck parking, tolls): normalize in the Actor, expose provenance on every row, keep math deterministic, let MCP handle the call, let the agent handle the question.&lt;/p&gt;

&lt;p&gt;Try it: European Fuel Station Prices API (CEE + DACH) (&lt;a href="https://apify.com/kondasviktor/cee-dach-fuel-intelligence" rel="noopener noreferrer"&gt;https://apify.com/kondasviktor/cee-dach-fuel-intelligence&lt;/a&gt;) — AT/SI/HR/RO need no key. MCP setup: Apify MCP docs (&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;/p&gt;

&lt;p&gt;&lt;em&gt;Viktor Kondas · [Apify profile](&lt;a href="https://apify.com/kondasviktor" rel="noopener noreferrer"&gt;https://apify.com/kondasviktor&lt;/a&gt;) · Built in Cursor with Apify MCP&lt;/em&gt;&lt;/p&gt;

</description>
      <category>apify</category>
      <category>mcp</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Event and nightlife demand intelligence with Eventbrite and Resident Advisor</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Mon, 14 Sep 2026 05:10:26 +0000</pubDate>
      <link>https://dev.to/apify/event-and-nightlife-demand-intelligence-with-eventbrite-and-resident-advisor-32op</link>
      <guid>https://dev.to/apify/event-and-nightlife-demand-intelligence-with-eventbrite-and-resident-advisor-32op</guid>
      <description>&lt;h2&gt;
  
  
  Event and nightlife demand intelligence with Eventbrite and Resident Advisor
&lt;/h2&gt;

&lt;p&gt;Event promoters, venue bookers, and artist managers usually track demand through instinct: which venues seem busy, which artists seem to be blowing up, which ticket prices seem too high or too low. That's fine until you're deciding whether to book a venue in a new city or price tickets for an unfamiliar market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event and nightlife demand intelligence&lt;/strong&gt; means turning scattered venue and promoter pages into one city-level dataset of event density, pricing, and lineup activity, instead of relying on instinct or manually checking dozens of individual pages. Eventbrite and Resident Advisor between them cover a large share of public event listings — general events and ticketing on one side, electronic music and nightlife specifically on the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use Eventbrite Scraper to collect event listings, dates, venues, and ticket prices across categories and cities. Use Resident Advisor Scraper for electronic music and nightlife events specifically, including artist lineups and venue details. Combine both to build a city-level view of event density, pricing, and which artists or venues are drawing repeat bookings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The event demand stack
&lt;/h2&gt;

&lt;p&gt;The stack uses two CrawlerBros Actors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/eventbrite-scraper" rel="noopener noreferrer"&gt;Eventbrite Scraper&lt;/a&gt;&lt;/strong&gt; for events including title, date, venue, ticket price, and tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/resident-advisor-scraper" rel="noopener noreferrer"&gt;Resident Advisor Scraper&lt;/a&gt;&lt;/strong&gt; for upcoming events by city, artists, and venues.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3rkkogm7bezeuinl76v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3rkkogm7bezeuinl76v.png" alt="Eventbrite Scraper Actor page on Apify, showing location, category, and date-range input fields used for event demand research" width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Target city or region&lt;br&gt;
    -&amp;gt; Eventbrite Scraper (general events, ticket pricing)&lt;br&gt;
    -&amp;gt; Resident Advisor Scraper (nightlife/music events, artists, venues)&lt;br&gt;
    -&amp;gt; city-level event demand dataset&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two platforms, two lenses&lt;/strong&gt;: Eventbrite covers broad event categories; Resident Advisor is concentrated in electronic music and nightlife specifically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event count alone misleads&lt;/strong&gt;: a city with fewer, higher-price events can be a healthier market than one with many low-price events competing for the same audience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Venue-level frequency matters more than city totals&lt;/strong&gt;: a venue hosting weekly events is a different business than one hosting quarterly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RA's sparse coverage of a city doesn't mean the city is quiet&lt;/strong&gt;: check Eventbrite's music category for that city before concluding demand is low.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;For Eventbrite, filter by city and category (music, food and drink, business, arts) rather than pulling every event type in one run, since category mix varies a lot by city and a single run mixing categories makes pricing comparisons meaningless.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "location": "Austin, TX",&lt;br&gt;
  "category": "music",&lt;br&gt;
  "maxItems": 150,&lt;br&gt;
  "dateRange": "next_30_days"&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Resident Advisor, scope by city and date range. RA's listings are concentrated in cities with an active electronic music scene, so a city with little RA coverage isn't necessarily quiet — it may just not be RA's core market, which is worth checking against Eventbrite's own music category for that city.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;The Eventbrite row needs event title, venue, date, category, ticket price (or price range), and organizer. The Resident Advisor row needs event title, venue, date, artist lineup, and event URL. Neither dataset alone answers "is this city's nightlife scene growing" — venue-level event frequency over several weeks does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 1: venue booking decisions for touring artists
&lt;/h2&gt;

&lt;p&gt;An artist manager deciding which cities to book can check RA event density and typical lineup tier for target cities, cross-referenced with Eventbrite's broader event calendar to see what else is competing for the same audience on a given date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 2: ticket pricing benchmarks
&lt;/h2&gt;

&lt;p&gt;A promoter pricing tickets for a new event can pull recent Eventbrite and RA listings for comparable events in the same city and genre to price against what's actually selling, rather than copying a price from a different market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 3: venue and city market research
&lt;/h2&gt;

&lt;p&gt;A venue considering a second location can compare event density and category mix across candidate cities — a city with steady RA and Eventbrite activity across multiple venues suggests an established scene; a city with only occasional large events suggests a market that hasn't been built out yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Don't compare cities on raw event count alone.&lt;/strong&gt; A city with fewer, higher-price events can represent a healthier market than a city with many low-price events competing for the same audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track venue-level frequency, not just city-level totals.&lt;/strong&gt; A venue hosting events weekly is a different business proposition than one hosting quarterly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combine both sources before drawing genre conclusions.&lt;/strong&gt; RA is strong for electronic music specifically; general demand for a city's live music scene needs Eventbrite's broader category coverage too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch date ranges carefully.&lt;/strong&gt; Both platforms list events well in advance, so a snapshot taken today under-represents a city's actual events happening next month unless the date range is wide enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One source alone under-represents demand.&lt;/strong&gt; Using RA data alone works for electronic-music-focused research but badly under-represents general live music and community events. Adding Eventbrite's broader category coverage gives a much more complete city picture.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Pricing data&lt;/th&gt;
&lt;th&gt;Artist/lineup data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual venue-by-venue checks&lt;/td&gt;
&lt;td&gt;Narrow, slow&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticketing platform dashboards&lt;/td&gt;
&lt;td&gt;Owned events only&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify Eventbrite + RA pipeline&lt;/td&gt;
&lt;td&gt;Multi-venue, multi-city&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (RA)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current Pricing tab on each Actor page before running at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify track event ticket pricing?
&lt;/h2&gt;

&lt;p&gt;Yes. Eventbrite Scraper extracts public event listings including ticket price, venue, and date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Resident Advisor only for electronic music?
&lt;/h2&gt;

&lt;p&gt;Yes, RA's coverage is concentrated in electronic music and nightlife; use Eventbrite for broader event category coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can this help decide where to book a tour?
&lt;/h2&gt;

&lt;p&gt;Yes — comparing event density, pricing, and lineup tier across candidate cities is a common use of this combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick one city and date range, run Eventbrite for the relevant category and Resident Advisor for the same city, then compare venue frequency and pricing.&lt;/p&gt;

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

&lt;p&gt;Pick one city and one date range. Run &lt;a href="https://apify.com/crawlerbros/eventbrite-scraper" rel="noopener noreferrer"&gt;Eventbrite Scraper&lt;/a&gt; for the relevant category and &lt;a href="https://apify.com/crawlerbros/resident-advisor-scraper" rel="noopener noreferrer"&gt;Resident Advisor Scraper&lt;/a&gt; for the same city. Compare venue frequency and ticket pricing across both. The city with steady activity across multiple venues, not just one big event, is usually the stronger long-term market.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>music</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Mining Quora and Reddit for SEO content ideas</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Sun, 13 Sep 2026 07:07:30 +0000</pubDate>
      <link>https://dev.to/apify/mining-quora-and-reddit-for-seo-content-ideas-3eob</link>
      <guid>https://dev.to/apify/mining-quora-and-reddit-for-seo-content-ideas-3eob</guid>
      <description>&lt;h2&gt;
  
  
  Mining Quora and Reddit for SEO content ideas
&lt;/h2&gt;

&lt;p&gt;Most SEO content calendars start from a keyword tool, which shows what people search for but not how they actually phrase the question or what specifically confuses them. Quora and Reddit answer that second part directly — both platforms are full of people asking exact questions in their own words and getting corrected, argued with, or thanked in the replies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content idea mining from Quora and Reddit&lt;/strong&gt; means turning a keyword into a specific, well-scoped article outline built from the actual questions people ask and the actual confusions that show up in the answers, instead of writing another generic "ultimate guide" from keyword volume alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use Quora Search Scraper to find questions and existing answers related to a topic. Use Reddit Scraper to pull relevant subreddit discussions and Reddit Comment Scraper to capture the actual reply threads. Combine all three to build content outlines around the specific questions people ask and the specific confusions that come up in the answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The content research stack
&lt;/h2&gt;

&lt;p&gt;The stack uses three CrawlerBros Actors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/quora-search-scraper" rel="noopener noreferrer"&gt;Quora Search Scraper&lt;/a&gt;&lt;/strong&gt; for questions, answers, and engagement metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/reddit-scraper" rel="noopener noreferrer"&gt;Reddit Scraper&lt;/a&gt;&lt;/strong&gt; for entire subreddits with posts, titles, text, scores, and timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/reddit-comment-scraper" rel="noopener noreferrer"&gt;Reddit Comment Scraper&lt;/a&gt;&lt;/strong&gt; for comments on specific posts with text, scores, and timestamps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxiz387fnlh8g1vmxthh3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxiz387fnlh8g1vmxthh3.png" alt="Quora Search Scraper Actor page on Apify, showing the search query input field used for finding questions and top answers on a topic" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The research loop:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Target topic or keyword&lt;br&gt;
    -&amp;gt; Quora Search Scraper (questions + top answers)&lt;br&gt;
    -&amp;gt; Reddit Scraper (relevant subreddit posts)&lt;br&gt;
    -&amp;gt; Reddit Comment Scraper (reply-thread detail on the best posts)&lt;br&gt;
    -&amp;gt; content outline backlog&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Question phrasing is a reusable asset.&lt;/strong&gt; Quora question text is often close to how someone would actually search — closer than a paraphrased keyword-tool phrase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comment threads reveal the real content gap.&lt;/strong&gt; Where commenters correct or argue with a post is a stronger signal of what's missing than keyword volume alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run comment extraction selectively.&lt;/strong&gt; Reddit Comment Scraper is worth running on the high-engagement posts Reddit Scraper already flagged, not on every post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search multiple phrasings of the same topic.&lt;/strong&gt; Quora questions rarely match keyword-tool phrasing exactly; two or three variations catch more relevant questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;For Quora Search Scraper, search by the same phrase variations you'd use for keyword research — Quora questions are often phrased more conversationally than search queries, so running a few phrasings of the same topic surfaces more relevant questions than a single exact-match search.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "searchQuery": "how to price a freelance web design project",&lt;br&gt;
  "maxItems": 50&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Reddit Scraper, target subreddits where your topic's actual audience discusses it, rather than a generic keyword search across all of Reddit — a freelance-pricing topic belongs in freelancing and web-design subreddits, not a broad site-wide search. For Reddit Comment Scraper, run it against the specific posts Reddit Scraper flagged as high-engagement, since comment-level detail is only worth extracting for posts that already show real discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;The Quora row needs question text, top answer text, answer author, and vote count — question phrasing is often the most reusable asset, since it's frequently close to how someone would actually search. The Reddit post row needs title, subreddit, score, comment count, and URL. The Reddit comment row needs comment text, score, and parent post URL, so a repeated objection or correction can be traced back to its source thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 1: FAQ and "People Also Ask"-style content
&lt;/h2&gt;

&lt;p&gt;Quora question phrasing translates almost directly into FAQ section headers — using the actual question text (rather than a paraphrased version) both matches searcher intent more closely and often matches long-tail search phrasing search engines already index well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 2: identifying content gaps in existing guides
&lt;/h2&gt;

&lt;p&gt;Reading Reddit comment threads on a topic often surfaces the specific point where people get confused or where an existing popular guide is wrong or outdated — that's a stronger content-gap signal than keyword volume alone, since it shows exactly what existing content is failing to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case 3: building content in the audience's actual language
&lt;/h2&gt;

&lt;p&gt;The phrasing patterns that repeat across Quora answers and Reddit comments — the specific words practitioners use, not marketing language — make useful headline and subheading material, since content written in the audience's own vocabulary tends to read as more credible to that audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Search multiple phrasings, not just the primary keyword.&lt;/strong&gt; Quora questions rarely match keyword-tool phrasing exactly; running two or three variations catches more relevant questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only run Reddit Comment Scraper on posts worth the detail.&lt;/strong&gt; Comment-level extraction is more useful selectively, on posts Reddit Scraper already flagged as high-engagement, than run broadly across every post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the actual disagreement in comment threads.&lt;/strong&gt; The most useful content ideas often come from where commenters correct or argue with the original post, not from the post itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep source links attached to every idea.&lt;/strong&gt; An outline built from a Reddit thread or Quora answer should keep the source URL nearby, both for fact-checking and to avoid presenting someone else's specific anecdote as a generic example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question text alone misses the texture of confusion.&lt;/strong&gt; Quora questions give headline ideas but miss where people actually get stuck. Adding Reddit comment-thread detail surfaces the specific misconceptions worth addressing directly in the content.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Question phrasing&lt;/th&gt;
&lt;th&gt;Discussion depth&lt;/th&gt;
&lt;th&gt;Scale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keyword tool only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual forum browsing&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify Quora + Reddit pipeline&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current Pricing tab on each Actor page before running at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify scrape Quora questions and answers?
&lt;/h2&gt;

&lt;p&gt;Yes. Quora Search Scraper extracts questions, answers, and engagement metrics for a given search query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this different from keyword research?
&lt;/h2&gt;

&lt;p&gt;Yes — keyword research shows search volume and phrasing at a high level; this workflow shows the specific questions and confusions behind that volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I need to scrape every comment on every post?
&lt;/h2&gt;

&lt;p&gt;No — target comment extraction at the specific high-engagement posts your subreddit-level scrape already flagged as relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick one topic, run Quora Search Scraper for two or three phrasings and Reddit Scraper on two or three relevant subreddits, then pull comments on the best posts.&lt;/p&gt;

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

&lt;p&gt;Pick one topic you're already planning to write about. Run &lt;a href="https://apify.com/crawlerbros/quora-search-scraper" rel="noopener noreferrer"&gt;Quora Search Scraper&lt;/a&gt; for two or three phrasings, then run &lt;a href="https://apify.com/crawlerbros/reddit-scraper" rel="noopener noreferrer"&gt;Reddit Scraper&lt;/a&gt; on the two or three subreddits where your audience actually hangs out. Pull comments with &lt;a href="https://apify.com/crawlerbros/reddit-comment-scraper" rel="noopener noreferrer"&gt;Reddit Comment Scraper&lt;/a&gt; on the best posts. Build your outline from the actual questions and disagreements, not from a keyword list alone.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>seo</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Startup idea radar: finding validated ideas with Reddit, Search, and News data</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Sat, 12 Sep 2026 06:55:11 +0000</pubDate>
      <link>https://dev.to/apify/startup-idea-radar-finding-validated-ideas-with-reddit-search-and-news-data-15a0</link>
      <guid>https://dev.to/apify/startup-idea-radar-finding-validated-ideas-with-reddit-search-and-news-data-15a0</guid>
      <description>&lt;h2&gt;
  
  
  Startup idea radar: finding validated ideas with Reddit, Search, and News data
&lt;/h2&gt;

&lt;p&gt;Startup ideas rarely arrive as neat pitch decks.&lt;/p&gt;

&lt;p&gt;They show up as complaints, repeated questions, "alternative to..." threads, half-solved workflows, and long comments explaining why an existing product almost works.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;startup idea radar&lt;/strong&gt; is a repeatable research workflow that collects public pain signals — Reddit complaints, Google search intent, and news momentum — and scores them, instead of relying on a single brainstorm or gut feeling. This Apify idea radar collects those signals from Reddit, Google Search, and Google News so a founder can rank pains before building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use Reddit scraping to find raw user complaints, Google Search scraping to understand search intent and competing content, and Google News scraping to track market momentum. Combine the datasets to rank startup ideas by pain frequency, buyer intent, competition, and trend activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The startup research stack
&lt;/h2&gt;

&lt;p&gt;The research stack uses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/reddit-keywords" rel="noopener noreferrer"&gt;Reddit Keywords&lt;/a&gt;&lt;/strong&gt; and Reddit Scraper-style Actors for problem discovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/google-search-results-scraper" rel="noopener noreferrer"&gt;Google Search Results Scraper&lt;/a&gt;&lt;/strong&gt; for search intent, People Also Ask, and competitor pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/google-news-scraper" rel="noopener noreferrer"&gt;Google News Scraper&lt;/a&gt;&lt;/strong&gt; for market news and trend validation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpifxp3fn07700uo64hfo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpifxp3fn07700uo64hfo.png" alt="Reddit Keywords Actor page on Apify, showing keyword search input fields used for startup idea and complaint discovery" width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea discovery loop:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Idea keywords&lt;br&gt;
    -&amp;gt; Reddit complaint mining&lt;br&gt;
    -&amp;gt; Google search intent&lt;br&gt;
    -&amp;gt; news and trend context&lt;br&gt;
    -&amp;gt; scored idea backlog&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 sources, 3 jobs&lt;/strong&gt;: Reddit finds pain, Google Search finds commercial intent, Google News finds market momentum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A signal is strongest when it appears in more than one source&lt;/strong&gt;: a Reddit complaint plus matching search queries plus category news is far more convincing than any one alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact phrases beat generic keywords&lt;/strong&gt;: complaint phrases like "alternative to" or "too expensive" surface pain that broad keyword volume misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boring, repeated pains tend to win&lt;/strong&gt;: reporting, lead enrichment, price tracking, hiring signals, and data cleanup outperformed novelty ideas in practice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reddit finds pain
&lt;/h2&gt;

&lt;p&gt;Reddit is useful because people complain in detail. They don't say "workflow optimization opportunity." They say the tool broke, the pricing is unfair, the integration is missing, or the support team ignored them.&lt;/p&gt;

&lt;p&gt;Search for patterns like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"alternative to"&lt;/li&gt;
&lt;li&gt;"is there a tool for"&lt;/li&gt;
&lt;li&gt;"I hate using"&lt;/li&gt;
&lt;li&gt;"too expensive"&lt;/li&gt;
&lt;li&gt;"doesn't integrate with"&lt;/li&gt;
&lt;li&gt;"manual spreadsheet"&lt;/li&gt;
&lt;li&gt;"looking for recommendations"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output gives threads, titles, scores, subreddits, URLs, and sometimes comment data depending on the Actor stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;For idea discovery, the exact query set matters more than the tool choice. Start Reddit Keywords with complaint and alternative phrases, then use Google Search Results Scraper for the same problem terms.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "keywords": [&lt;br&gt;
    "alternative to airtable",&lt;br&gt;
    "too expensive crm",&lt;br&gt;
    "is there a tool for client reporting",&lt;br&gt;
    "manual spreadsheet workflow"&lt;br&gt;
  ],&lt;br&gt;
  "resultLimit": 100,&lt;br&gt;
  "sort": "relevance"&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then pass the strongest terms into Google Search and Google News. Search shows whether the problem has commercial intent. News shows whether the category is moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;For each idea, keep both the evidence and the score:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "idea": "client reporting automation for agencies",&lt;br&gt;
  "source": "reddit",&lt;br&gt;
  "query": "manual spreadsheet client reporting",&lt;br&gt;
  "title": "How are you all sending weekly client reports?",&lt;br&gt;
  "subreddit": "agency",&lt;br&gt;
  "score": 87,&lt;br&gt;
  "commentsCount": 42,&lt;br&gt;
  "url": "https://old.reddit.com/r/..."&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The score is never enough on its own. Keep the original title, source, and URL because the wording is often the insight. A founder can learn more from one angry thread than from a sterile keyword-volume number.&lt;/p&gt;

&lt;p&gt;In a small Reddit Keywords run for "alternative to airtable", the Actor returned five posts. The output included &lt;code&gt;keyword&lt;/code&gt;, &lt;code&gt;post_id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;subreddit&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;num_comments&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;old_reddit_url&lt;/code&gt;, &lt;code&gt;link_flair&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;is_stickied&lt;/code&gt;, and &lt;code&gt;is_nsfw&lt;/code&gt;. This is the exact kind of public pain signal you want for idea validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Search finds intent
&lt;/h2&gt;

&lt;p&gt;Reddit pain is not enough. Some complaints are loud but not commercial.&lt;/p&gt;

&lt;p&gt;Google Search Results Scraper helps check whether people also search for the problem. Look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;organic competitors&lt;/li&gt;
&lt;li&gt;People Also Ask questions&lt;/li&gt;
&lt;li&gt;related searches&lt;/li&gt;
&lt;li&gt;result count&lt;/li&gt;
&lt;li&gt;ranking page types&lt;/li&gt;
&lt;li&gt;country-specific differences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Reddit has pain and Google has commercial pages, the idea becomes more interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  News finds momentum
&lt;/h2&gt;

&lt;p&gt;Google News adds market context. A boring problem in a growing market can be more valuable than an exciting problem in a shrinking one.&lt;/p&gt;

&lt;p&gt;Monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;funding announcements&lt;/li&gt;
&lt;li&gt;regulation changes&lt;/li&gt;
&lt;li&gt;layoffs&lt;/li&gt;
&lt;li&gt;category growth&lt;/li&gt;
&lt;li&gt;enterprise adoption&lt;/li&gt;
&lt;li&gt;platform policy shifts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps separate durable markets from temporary noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to score ideas
&lt;/h2&gt;

&lt;p&gt;A lightweight scoring model uses five dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pain frequency&lt;/li&gt;
&lt;li&gt;pain intensity&lt;/li&gt;
&lt;li&gt;buyer intent&lt;/li&gt;
&lt;li&gt;competitive saturation&lt;/li&gt;
&lt;li&gt;market momentum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A great idea doesn't need a perfect score. It needs enough evidence to justify deeper research.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a good signal looks like
&lt;/h2&gt;

&lt;p&gt;A useful idea signal usually appears in more than one source.&lt;/p&gt;

&lt;p&gt;Reddit might show people complaining that existing tools are too expensive. Google Search might show "best X alternative" and "X pricing" queries. Google News might show funding or regulation in the category. Together, those signals suggest a market with pain, intent, and movement.&lt;/p&gt;

&lt;p&gt;A weak signal looks different. One viral Reddit thread may create noise, but if Google has no related searches and news is quiet, the pain may be too narrow. That doesn't mean the idea is bad — it means you need interviews before treating it as a market.&lt;/p&gt;

&lt;p&gt;Also look for language patterns. The exact words people use in complaints can become landing page copy later. "I just want a simple way to..." is more valuable than a generic keyword because it captures the job-to-be-done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The research backlog
&lt;/h2&gt;

&lt;p&gt;Every idea becomes a row in a backlog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idea name&lt;/li&gt;
&lt;li&gt;source keywords&lt;/li&gt;
&lt;li&gt;strongest Reddit threads&lt;/li&gt;
&lt;li&gt;strongest search queries&lt;/li&gt;
&lt;li&gt;related competitors&lt;/li&gt;
&lt;li&gt;recent news links&lt;/li&gt;
&lt;li&gt;pain score&lt;/li&gt;
&lt;li&gt;intent score&lt;/li&gt;
&lt;li&gt;notes from manual review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the workflow practical. The goal is not to automate taste. The goal is to make your taste better informed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Apify helps
&lt;/h2&gt;

&lt;p&gt;The advantage of Apify is repeatability. A founder can run the same research workflow every week without rebuilding scrapers or copying search results by hand.&lt;/p&gt;

&lt;p&gt;That matters because startup research changes. A pain point that looked small in January may explode after a platform policy change in March. A category that looked crowded may become interesting when users start complaining about pricing. Scheduled Actor runs create a trail of evidence instead of a pile of one-off notes.&lt;/p&gt;

&lt;p&gt;Manual reading of the best threads is still worth it. Automation narrows the field. Judgment picks the idea. The best outcome is not more ideas — it's fewer, better-tested ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Search exact phrases.&lt;/strong&gt; Generic keywords produce generic results. Complaint phrases reveal pain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep subreddit context.&lt;/strong&gt; A complaint in a founder subreddit means something different from the same complaint in a hobby subreddit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read comments, not only titles.&lt;/strong&gt; The best details are usually in comment threads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not confuse trendiness with demand.&lt;/strong&gt; News volume can mean hype. Pair it with search and Reddit evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store rejected ideas.&lt;/strong&gt; Rejected ideas are useful when the market changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Novelty isn't the goal.&lt;/strong&gt; The best ideas were rarely the weirdest ones — they were boring problems with repeated pain: reporting, lead enrichment, price tracking, hiring signals, and data cleanup. Scraping helps you see repetition, which is usually more valuable than surprise.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Founder intuition&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Biased&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surveys&lt;/td&gt;
&lt;td&gt;Direct answers&lt;/td&gt;
&lt;td&gt;Slow and prompted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword tools&lt;/td&gt;
&lt;td&gt;Search volume&lt;/td&gt;
&lt;td&gt;Weak on raw pain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify idea radar&lt;/td&gt;
&lt;td&gt;Public pain plus intent&lt;/td&gt;
&lt;td&gt;Needs human judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current Actor Pricing tabs before running at scale. This workflow is usually cost-controlled by small, targeted runs instead of broad scraping.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can scraping help validate startup ideas?
&lt;/h2&gt;

&lt;p&gt;Yes. Scraping public discussions, search results, and news can reveal recurring problems and market signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use Reddit for startup research?
&lt;/h2&gt;

&lt;p&gt;Reddit has detailed public discussions where users describe problems in their own words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this replace customer interviews?
&lt;/h2&gt;

&lt;p&gt;No. It helps you choose better ideas before interviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick one market, run Reddit keyword searches for complaint phrases, scrape Google Search results for the same problem, and collect recent news.&lt;/p&gt;

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

&lt;p&gt;Pick one market. Run &lt;a href="https://apify.com/crawlerbros/reddit-keywords" rel="noopener noreferrer"&gt;Reddit Keywords&lt;/a&gt; for complaint phrases, scrape &lt;a href="https://apify.com/crawlerbros/google-search-results-scraper" rel="noopener noreferrer"&gt;Google Search Results Scraper&lt;/a&gt; for the same problem, and collect recent news with &lt;a href="https://apify.com/crawlerbros/google-news-scraper" rel="noopener noreferrer"&gt;Google News Scraper&lt;/a&gt;. If all three sources point to the same pain, you have something worth investigating.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>startup</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Turning a jobs scraper into an AI agent's tool with the Apify MCP server</title>
      <dc:creator>Zahar Baranenko</dc:creator>
      <pubDate>Fri, 11 Sep 2026 12:07:54 +0000</pubDate>
      <link>https://dev.to/apify/turning-a-jobs-scraper-into-an-ai-agents-tool-with-the-apify-mcp-server-2c04</link>
      <guid>https://dev.to/apify/turning-a-jobs-scraper-into-an-ai-agents-tool-with-the-apify-mcp-server-2c04</guid>
      <description>&lt;p&gt;There is a lot of good advice around on making your Actor agent-friendly. Almost none of it shows you the thing an agent actually sends over the wire — and that turned out to be the part that changed how I build.&lt;/p&gt;

&lt;p&gt;I build Actors on Apify under the name primeflowio. One of them, &lt;a href="https://apify.com/primeflowio/ats-jobs-scraper" rel="noopener noreferrer"&gt;ATS Jobs Scraper &amp;amp; Change Monitor&lt;/a&gt;, takes a list of companies and returns every open job they have, normalized into one schema — no matter which applicant tracking system the company uses. A few weeks after publishing it, I connected it to the Apify MCP server and an agent called it like a native tool: discovered it, filled the input, ran it, read the results back. No glue code on my side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5paczdlxfsy5ug5odmgt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5paczdlxfsy5ug5odmgt.png" alt="Apify Console run history with the Origin column reading MCP" width="800" height="312"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Origin column says MCP: that run was started by an agent, not by me clicking Start.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So this article does two things. First it walks the protocol by hand — every request, every real response — so you can see exactly what your Actor is answering to. Then it goes through what I changed once I had read those responses, including a bug that would have told a customer that every job at a company had disappeared overnight.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem the Actor solves
&lt;/h2&gt;

&lt;p&gt;Job postings are scattered across ATS platforms. If you track 200 target companies — as recruiters, B2B sales teams, and serious job seekers do — you'd need to visit 200 career pages, sitting on Workday, Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee or Personio, each with its own markup.&lt;/p&gt;

&lt;p&gt;The twist most people miss: you don't need to scrape any of those pages. The major ATS platforms expose documented, public endpoints for their job boards. Here is every platform the Actor supports, with what each one actually returned when I re-checked it by hand this week:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Live check (Sep 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boards-api.greenhouse.io/v1/boards/{slug}/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stripe: 611, gitlab: 230&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.lever.co/v0/postings/{slug}?mode=json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;spotify: 73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.ashbyhq.com/posting-api/job-board/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ramp: 143, linear: 28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personio&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{company}.jobs.personio.de/xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;personio: 1 open role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recruitee&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{company}.recruitee.com/api/offers/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yource: 1 open role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{tenant}.{wd}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nvidia: 2,000 — and slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.smartrecruiters.com/v1/companies/{slug}/postings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;see below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No API keys, no proxies, no anti-bot arms race. The hard part isn't access — it's detection (which ATS does this company use?), normalization (a different response shape per platform, one schema out), and change tracking.&lt;/p&gt;

&lt;p&gt;Three of those rows deserve a footnote, and they are the same lesson in different clothes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workday is the expensive one.&lt;/strong&gt; It answers, and it answers with everything — two thousand postings for one tenant — but a small run against it took 83 seconds where Ashby takes two. That is fine for a watchlist on a schedule and a bad idea for anything with a stopwatch on it, which is why my prefilled input does not point at Workday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recruitee gets the failure mode right, and it is worth copying.&lt;/strong&gt; An unknown company returns &lt;code&gt;404 {"error":"Not Found"}&lt;/code&gt;. A real company with nothing open returns &lt;code&gt;200 {"offers":[]}&lt;/code&gt;. Those are different sentences, and a caller can act on the difference.&lt;/p&gt;

&lt;p&gt;Which brings us to SmartRecruiters, which does not. It answers &lt;code&gt;200 {"totalFound": 0, "content": []}&lt;/code&gt; for every identifier I tried — including companies that visibly run their careers site on SmartRecruiters, and including identifiers that do not exist at all. An endpoint that cannot distinguish "no openings" from "wrong name" from "this company never switched the public API on" is not a data source; it is a coin flip with a JSON wrapper. The Actor now reports that board as unreadable instead of returning an empty list that looks like an answer — which turns out to be the same bug I had shipped myself, one layer up. More on that in lesson 6.&lt;/p&gt;

&lt;p&gt;So the Actor does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Auto-detect&lt;/strong&gt; — given a company slug or careers URL, it probes the supported endpoints and figures out which ATS answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize&lt;/strong&gt; — every job becomes one record: &lt;code&gt;title, department, team, location, remote, employment_type, salary_min/max, url, published_at, updated_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; — in &lt;code&gt;monitor&lt;/code&gt; mode it keeps the previous run's state in the Actor's key-value store and emits &lt;code&gt;new&lt;/code&gt; / &lt;code&gt;removed&lt;/code&gt; / &lt;code&gt;changed&lt;/code&gt; records as a diff. Users point a webhook or an n8n flow at those and get "company X just opened 3 engineering roles" without re-downloading everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last part is the reason the Actor exists at all: the incumbent job-data Actors make you re-pull the full dataset every run and charge per job every time. If what you actually want is &lt;em&gt;changes&lt;/em&gt;, you're paying for the same rows daily.&lt;/p&gt;
&lt;h2&gt;
  
  
  Every Actor is already an MCP tool
&lt;/h2&gt;

&lt;p&gt;Here's the part that cost me zero extra work. The &lt;a href="https://mcp.apify.com" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt; exposes any public Actor as a tool over the Model Context Protocol. You don't register anything, you don't write a manifest — the Actor's input schema &lt;em&gt;is&lt;/em&gt; the tool definition. An agent connects to:&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=primeflowio/ats-jobs-scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and gets a tool named &lt;code&gt;primeflowio--ats-jobs-scraper&lt;/code&gt;, with parameters generated from the same input schema that renders the form in Apify Console. (&lt;code&gt;?tools=&lt;/code&gt; is the parameter in Apify's docs; &lt;code&gt;?actors=&lt;/code&gt; is accepted too — I checked both today and they return the same five tools.)&lt;/p&gt;

&lt;p&gt;I wanted to see exactly what an agent sees, so I did the whole handshake by hand with curl before touching any framework. MCP over streamable HTTP is three steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Initialize a session:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://mcp.apify.com/?tools=primeflowio/ats-jobs-scraper"&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;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&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"&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;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-03-26","capabilities":{},
                 "clientInfo":{"name":"demo-agent","version":"1.0"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response headers contain &lt;code&gt;mcp-session-id&lt;/code&gt; — every later call carries it in an &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header, plus a &lt;code&gt;notifications/initialized&lt;/code&gt; message to finish the handshake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. List tools.&lt;/strong&gt; &lt;code&gt;{"method":"tools/list"}&lt;/code&gt; returns, in my session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;primeflowio--ats-jobs-scraper
get-actor-run
get-dataset-items
get-key-value-store-record
abort-actor-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what the server adds for free: the agent doesn't just get my Actor, it gets the helpers to check a run's status and page through the results dataset. That completes the loop — an agent can start a run, poll it, and fetch data without any Apify-specific code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Call the tool:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://mcp.apify.com/?tools=primeflowio/ats-jobs-scraper"&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;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&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"&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;"Accept: application/json, text/event-stream"&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;"Mcp-Session-Id: &lt;/span&gt;&lt;span class="nv"&gt;$SESSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"primeflowio--ats-jobs-scraper",
                 "arguments":{"companies":["https://jobs.lever.co/spotify"],
                              "mode":"pull","maxJobsPerCompany":3}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run finished in under two seconds and the MCP response came back with a summary the agent can act on directly:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"SUCCEEDED"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"statusMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1/1 companies, 3 jobs, 0 changes, 0 errors"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;SUCCEEDED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.635&lt;/span&gt;&lt;span class="err"&gt;s.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;items;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;available.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get-dataset-items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;datasetId=pl&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;nClKueN&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;efCO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;limit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fetch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;total).&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Available&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(dot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notation):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;record_type,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;company,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ats,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;job_id,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;title,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;department,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;team,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;location,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;remote,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;employment_type,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;salary_min,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;salary_max,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;salary_currency,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;url,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki3vjd320lydcul1tq2c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki3vjd320lydcul1tq2c.png" alt="Terminal output of the Python MCP client: the tool list and the run summary" width="800" height="188"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The whole exchange from a plain client: five tools discovered, one called, a summary back in under three seconds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read that response again from an LLM's point of view. It's not a blob of JSON rows — it's an instruction: &lt;em&gt;here's what happened, here's the tool call to make next, here are the fields you can ask for.&lt;/em&gt; The Apify MCP server writes that guidance itself. My only job was to make sure the run summary (&lt;code&gt;statusMessage&lt;/code&gt;) and the dataset fields were worth reading.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same flow in Python
&lt;/h2&gt;

&lt;p&gt;For anything scripted I use ~60 lines of plain &lt;code&gt;requests&lt;/code&gt; — no MCP SDK needed for the happy path. Full file in the repo; the core is:&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="n"&gt;MCP_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://mcp.apify.com/?tools=primeflowio/ats-jobs-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&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;jsonrpc&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;2.0&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;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&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="n"&gt;params&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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="n"&gt;msg_id&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mcp-Session-Id&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="n"&gt;session&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MCP_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&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="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;

&lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;initialize&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;msg_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-session-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notifications/initialized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&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;name&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;primeflowio--ats-jobs-scraper&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;arguments&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;companies&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://jobs.lever.co/spotify&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;pull&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;maxJobsPerCompany&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One implementation detail that trips people up: responses arrive as server-sent events (&lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;), even for a single JSON-RPC reply. &lt;code&gt;parse_sse&lt;/code&gt; just takes the last &lt;code&gt;data:&lt;/code&gt; line and &lt;code&gt;json.loads&lt;/code&gt; it.&lt;/p&gt;

&lt;p&gt;Plugging the same URL into Claude, Cursor, or any MCP-capable client is a config entry instead of code — the point of the protocol is that the curl session above is exactly what those clients do under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building for agents changed in my Actor design
&lt;/h2&gt;

&lt;p&gt;I originally designed the input schema for humans filling a form in Apify Console. Agents read the same schema, but they punish different mistakes. Six lessons I'd apply to any Actor now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The input schema is a tool contract — keep required fields to one.&lt;/strong&gt; My schema has exactly one required field, &lt;code&gt;companies&lt;/code&gt;. Everything else (&lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;maxJobsPerCompany&lt;/code&gt;, &lt;code&gt;includeSnapshot&lt;/code&gt;) has a sane default. An agent that has to guess six required parameters will guess some of them wrong; an agent that has to provide one list of company names almost can't fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Give agents a cost throttle.&lt;/strong&gt; &lt;code&gt;maxJobsPerCompany&lt;/code&gt; exists so a cautious caller can cap the bill of an exploratory run. My test cost under a cent: with pay-per-event pricing, 1 company check plus 3 job results is $0.005. An agent (or the human supervising it) can try the tool for pocket change before pointing it at a 500-company watchlist. If your Actor can generate unbounded output, give the caller a knob to bound it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Make &lt;code&gt;statusMessage&lt;/code&gt; machine-readable prose.&lt;/strong&gt; &lt;code&gt;"1/1 companies, 3 jobs, 0 changes, 0 errors"&lt;/code&gt; is short enough to fit in any context window and precise enough that an agent can decide what to do next without fetching the dataset at all. I treat that one string as part of the API surface now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Put bulk data in the dataset, not the response.&lt;/strong&gt; The MCP server already nudges this: it returns a summary plus "use &lt;code&gt;get-dataset-items&lt;/code&gt; with this ID" rather than inlining rows. Fighting that — returning huge payloads inline — wastes the agent's context and your users' tokens. Design the output so the first page of dataset items answers the most common question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnacpkhnp2o20fhzirv3r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnacpkhnp2o20fhzirv3r.png" alt="Normalized job records in the run's dataset: company, ATS, title, department, location, employment type" width="800" height="235"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One schema out, whichever ATS answered. This is the page the agent fetches after the summary tells it what's there.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Your defaults are load-bearing: the platform runs them.&lt;/strong&gt; Apify's automated QA runs every public Actor with its &lt;strong&gt;prefilled input&lt;/strong&gt; and expects success within about 5 minutes. Mine was &lt;code&gt;["linear", "https://jobs.lever.co/spotify"]&lt;/code&gt; — two boards, ~130 postings, chosen back when I was thinking about humans clicking "Start" and wanting to see something substantial. In August an upstream endpoint got slow, three consecutive QA runs failed, and the Actor was flagged "under maintenance" in the Store until the runs went green again. I found out from the notification emails, days later. Worth knowing what that clock is: under Apify's publishing terms, an Actor left broken for 30 days can be deprecated and removed.&lt;/p&gt;

&lt;p&gt;The lesson isn't "make the Actor faster". It's that the prefill is a health check the platform runs on your behalf, so it should exercise the cheapest, most reliable path you have. I've since cut mine to a single small board: 28 postings, 2.2 seconds against a five-minute budget. The same defaults make agent test-calls cheap, which is a happy coincidence — QA bots and cautious agents want exactly the same thing from you.&lt;/p&gt;

&lt;p&gt;Ivan Solovyev landed on the same wall from a different direction — &lt;a href="https://dev.to/apify/my-actors-worked-fine-until-an-ai-agent-called-them-4b2k"&gt;his prefill hit four marketplaces at once&lt;/a&gt; and the test gave up waiting while paying users ran the Actor happily all day. Two of us tripped over the same default in the same month, which says something about how easy it is to write a prefill for the demo rather than for the robot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. An empty answer is not "everything disappeared".&lt;/strong&gt; This one I found because of that article, and it is the most expensive bug I have shipped.&lt;/p&gt;

&lt;p&gt;In monitor mode the Actor compares this run's jobs against the previous run's state and emits &lt;code&gt;new&lt;/code&gt; / &lt;code&gt;removed&lt;/code&gt; / &lt;code&gt;changed&lt;/code&gt;. The failure path was already handled: if a job board errors out, the run records an &lt;code&gt;error&lt;/code&gt; for that company, keeps the old state, and moves on. What I had not separated was a board that answers &lt;strong&gt;200 with an empty list&lt;/strong&gt; — which happens during migrations, when a board is briefly unpublished, or when the provider hiccups.&lt;/p&gt;

&lt;p&gt;That path was &lt;code&gt;ok, jobs = True, []&lt;/code&gt;. The diff then did exactly what it was told: marked every known job as &lt;code&gt;removed&lt;/code&gt;, and overwrote the state with nothing. A customer watching Stripe would have received "611 jobs removed" one morning and "611 jobs new" the next, with every webhook firing twice on fiction.&lt;/p&gt;

&lt;p&gt;The guard is small, and the shape of it generalises to any stateful Actor:&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="c1"&gt;# A board that answers 200 with zero jobs is usually the source blinking, not
# every role closing at once. Keep the old state, say so, and only believe the
# zero once it repeats.
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prev_jobs&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;EMPTY_RUNS_BEFORE_TRUSTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;push_items&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_type&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;warning&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;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ats&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; returned an empty board while &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                           &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; jobs were known; previous state &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                           &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kept, no changes emitted&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;empty_streak&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
    &lt;span class="nf"&gt;state_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_key&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;ats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prev_jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty_streak&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested it by seeding the state with two known jobs for a board that currently answers empty. Before: two &lt;code&gt;removed&lt;/code&gt; records and a wiped state. After: &lt;code&gt;1/1 companies, 0 jobs, 0 changes, 0 errors, 1 stale&lt;/code&gt;, one &lt;code&gt;warning&lt;/code&gt; record, and both jobs still in state. Real mass closures are slow; sources blink instantly — so a zero has to earn its credibility over three consecutive runs before the Actor will act on it.&lt;/p&gt;

&lt;p&gt;The general rule, which is the same one Solovyev arrives at from the billing side: &lt;strong&gt;an Actor with memory must distinguish what the world says from what it managed to hear.&lt;/strong&gt; For a stateless scraper an empty result is a result. For anything that diffs, an empty result is an accusation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next: monitor mode plus agents
&lt;/h2&gt;

&lt;p&gt;Pull mode answers "who is company X hiring right now?" — a natural one-shot agent query ("did Spotify open any Android roles this month?"). Monitor mode is more interesting for agent workflows because it inverts the direction: instead of the agent polling, the Actor runs on Apify's scheduler, emits only &lt;code&gt;new&lt;/code&gt; / &lt;code&gt;removed&lt;/code&gt; / &lt;code&gt;changed&lt;/code&gt; records, and a webhook or n8n flow wakes the agent only when something changed. "Head of Data role appeared at a target account" is a buying signal; an agent that gets &lt;em&gt;only diffs&lt;/em&gt; can react to it without burning tokens re-reading yesterday's 611 postings.&lt;/p&gt;

&lt;p&gt;One platform detail makes that possible, and it is the part I expected to be hard. Monitor mode needs memory that survives between runs, and those runs belong to whoever calls the Actor, not to me. Actors run with limited permissions by default, which sounds like the end of the idea. The docs say a limited-permissions Actor may "create any additional storage, and write to that storage" and "read and write to storages created in previous runs", and my own runs — the log line reads &lt;code&gt;Running under "LIMITED_PERMISSIONS"&lt;/code&gt; — create a named key-value store, then read that same state back on later runs and diff against it. So the state can live on the caller's account rather than in a database of mine, and I never have to ask anyone for a token. I have only exercised that on my own account, so treat the cross-account half as documented rather than as something I measured; the design decision it points to is what matters. If you are building anything stateful, look at named storage before you reach for infrastructure of your own.&lt;/p&gt;

&lt;p&gt;The diff mode earns its keep on live data, by the way. While I was writing this, a watchlist run caught Databricks editing a marketing role 29 minutes after they touched it — one &lt;code&gt;changed&lt;/code&gt; record out of thirteen companies and roughly three and a half thousand postings. That is the argument for a change feed in a single line: the agent reads one row instead of 3,500.&lt;/p&gt;

&lt;p&gt;That's the pattern I'd summarize the whole experience with: the MCP server made my Actor callable by agents in an afternoon, but making it &lt;em&gt;worth calling&lt;/em&gt; — one required input, bounded costs, a status line an LLM can parse, diffs instead of dumps — was ordinary API design, applied to a new consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need to modify an Actor to expose it via MCP?&lt;/strong&gt; No. Any public Actor works through &lt;code&gt;mcp.apify.com&lt;/code&gt; immediately; the input schema becomes the tool signature. Whether it's a &lt;em&gt;good&lt;/em&gt; tool is a design question — see the six lessons above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does a call cost?&lt;/strong&gt; Whatever the Actor's pricing says — same as running it from Console or API. This Actor is pay-per-event: $2 per 1,000 companies checked, $1 per 1,000 job results, so the demo call above cost $0.005.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is scraping ATS job boards allowed?&lt;/strong&gt; The endpoints used here are the platforms' own public job-board APIs — Greenhouse Job Board API, Lever Postings API, Ashby Posting API, Recruitee's offers endpoint, Personio's job XML feed. They exist precisely so postings can be syndicated to aggregators and career-site widgets, which is why none of them need a key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the agent also fetch the results?&lt;/strong&gt; Yes — the MCP session includes &lt;code&gt;get-actor-run&lt;/code&gt; and &lt;code&gt;get-dataset-items&lt;/code&gt; helpers automatically, so the agent can page through results without any Apify SDK.&lt;/p&gt;

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

&lt;p&gt;Point an agent at your own watchlist. Add the MCP server to any MCP-capable client:&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=primeflowio/ats-jobs-scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…then ask it something you'd actually want to know — "who is Stripe hiring in Berlin right now?" — and watch it pick the arguments out of the input schema on its own. A single company check plus a handful of postings costs half a cent, so the first call is cheap enough to be curious with. Run it a second time a week later with &lt;code&gt;mode: "monitor"&lt;/code&gt; and you get the diff instead of the dump.&lt;/p&gt;

&lt;p&gt;The Actor is &lt;a href="https://apify.com/primeflowio/ats-jobs-scraper" rel="noopener noreferrer"&gt;ATS Jobs Scraper &amp;amp; Change Monitor&lt;/a&gt; on Apify Store. The demo client and the full curl transcript from this article are at &lt;a href="https://github.com/strongboxr/ats-mcp-agent-demo" rel="noopener noreferrer"&gt;github.com/strongboxr/ats-mcp-agent-demo&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build data Actors on Apify as &lt;a href="https://apify.com/primeflowio" rel="noopener noreferrer"&gt;primeflowio&lt;/a&gt;. If a number in this article has a caveat, the caveat is in the sentence next to it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggested meta description:&lt;/strong&gt; How an Apify Actor that scrapes ATS job boards became an AI agent tool via the Apify MCP server — the full handshake, working code, and six design lessons.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Shopify competitor monitoring without a price intelligence SaaS</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Fri, 11 Sep 2026 05:29:56 +0000</pubDate>
      <link>https://dev.to/apify/shopify-competitor-monitoring-without-a-price-intelligence-saas-1692</link>
      <guid>https://dev.to/apify/shopify-competitor-monitoring-without-a-price-intelligence-saas-1692</guid>
      <description>&lt;h2&gt;
  
  
  Shopify competitor monitoring without a price intelligence SaaS
&lt;/h2&gt;

&lt;p&gt;Most e-commerce competitor research is too manual to stay current.&lt;/p&gt;

&lt;p&gt;Someone checks competitor stores when a launch is coming. Someone copies prices into a sheet. Someone screenshots a sale page. Two weeks later, the data is already stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopify competitor monitoring&lt;/strong&gt; is the practice of collecting a competitor's public product catalog — prices, variants, tags, availability, and discounts — on a schedule and diffing it week over week, instead of manually checking storefronts when a launch seems likely. For Shopify stores, this is especially avoidable: many storefronts expose public catalog data that can be collected with &lt;strong&gt;Shopify Scraper Pro&lt;/strong&gt; on Apify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;You can monitor Shopify competitors by scraping public product catalogs, collections, prices, variants, vendors, tags, availability, and discount data with Shopify Scraper Pro on Apify. Run it on a schedule and compare datasets to detect price changes, new products, removed products, and sale campaigns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shopify monitoring setup
&lt;/h2&gt;

&lt;p&gt;The monitoring setup uses &lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/shopify-scraper-pro" rel="noopener noreferrer"&gt;Shopify Scraper Pro&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Actor supports multi-store batches, collection-scoped scraping, in-store search, sale tracking, endpoint fallbacks, image size controls, and filters for price, vendor, type, tags, title, sale status, availability, and dates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqn8tq57x8a1ctsmr33q7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqn8tq57x8a1ctsmr33q7.png" alt="Shopify Scraper Pro Actor page on Apify, showing catalog and sale-monitoring input options for store URLs, collections, and discount filters" width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The monitoring loop:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Competitor store list&lt;br&gt;
    -&amp;gt; Shopify Scraper Pro&lt;br&gt;
    -&amp;gt; product catalog dataset&lt;br&gt;
    -&amp;gt; weekly diff&lt;br&gt;
    -&amp;gt; price and launch alerts&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The most useful outputs are product title, handle, URL, vendor, type, tags, price, compare-at price, discount percent, availability, variants, images, and collection context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Handles, not titles, are the stable anchor.&lt;/strong&gt; Product titles change for merchandising reasons; handles usually stay stable across weekly diffs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog-level scraping catches launches, not just price moves.&lt;/strong&gt; Product-page-only monitoring fails the moment a competitor ships something new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sale status is derived, not typed.&lt;/strong&gt; Many stores never write "sale" in the title — &lt;code&gt;onSale&lt;/code&gt;, &lt;code&gt;comparePrice&lt;/code&gt;, and &lt;code&gt;discountPercent&lt;/code&gt; come from structured pricing fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collections often matter more than price.&lt;/strong&gt; A product moving into "best sellers" or "new arrivals" can be a stronger signal than a two-dollar price change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;Shopify Scraper Pro accepts &lt;code&gt;storeUrls&lt;/code&gt; as full URLs, bare domains, or &lt;code&gt;myshopify.com&lt;/code&gt; handles. For focused monitoring, &lt;code&gt;collectionHandles&lt;/code&gt; can restrict the crawl to collections like &lt;code&gt;sale&lt;/code&gt;, &lt;code&gt;new-arrivals&lt;/code&gt;, or &lt;code&gt;mens&lt;/code&gt;. The Actor also supports &lt;code&gt;searchQuery&lt;/code&gt;, &lt;code&gt;sortBy&lt;/code&gt;, &lt;code&gt;maxItemsPerStore&lt;/code&gt;, &lt;code&gt;maxItems&lt;/code&gt;, image quality controls, collection enrichment, availability filters, price filters, tag filters, &lt;code&gt;onSaleOnly&lt;/code&gt;, &lt;code&gt;minDiscountPercent&lt;/code&gt;, date filters, SKU filtering, and polite request delay.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "storeUrls": ["allbirds.com", "kith.com"],&lt;br&gt;
  "collectionHandles": ["new-arrivals", "sale"],&lt;br&gt;
  "sortBy": "best-selling",&lt;br&gt;
  "maxItemsPerStore": 250,&lt;br&gt;
  "onSaleOnly": false,&lt;br&gt;
  "minDiscountPercent": 20,&lt;br&gt;
  "enrichWithCollections": true,&lt;br&gt;
  "requestDelaySecs": 1&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That level of filtering is why catalog scraping beats random page checks — you can build a tight monitoring job around exactly the competitor collections and product signals that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;A product row needs enough detail to survive comparison over time. Keep &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;handle&lt;/code&gt;, &lt;code&gt;productUrl&lt;/code&gt;, &lt;code&gt;vendor&lt;/code&gt;, &lt;code&gt;productType&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;compareAtPrice&lt;/code&gt;, &lt;code&gt;discountPercent&lt;/code&gt;, &lt;code&gt;available&lt;/code&gt;, and &lt;code&gt;tags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;handle&lt;/code&gt; is the anchor. Titles and descriptions change often, but handles usually stay stable enough for weekly diffs.&lt;/p&gt;

&lt;p&gt;In a small Shopify Scraper Pro run against &lt;code&gt;allbirds.com&lt;/code&gt;, the Actor returned five product rows. The output included &lt;code&gt;productId&lt;/code&gt;, &lt;code&gt;handle&lt;/code&gt;, &lt;code&gt;productUrl&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;vendor&lt;/code&gt;, &lt;code&gt;productType&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;comparePrice&lt;/code&gt;, &lt;code&gt;onSale&lt;/code&gt;, &lt;code&gt;discountPercent&lt;/code&gt;, &lt;code&gt;variantCount&lt;/code&gt;, &lt;code&gt;available&lt;/code&gt;, &lt;code&gt;availableVariantCount&lt;/code&gt;, &lt;code&gt;skus&lt;/code&gt;, &lt;code&gt;variants&lt;/code&gt;, &lt;code&gt;images&lt;/code&gt;, &lt;code&gt;storeUrl&lt;/code&gt;, and &lt;code&gt;scrapedAt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Shopify is a good monitoring target
&lt;/h2&gt;

&lt;p&gt;Shopify stores change constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;new products launch&lt;/li&gt;
&lt;li&gt;variants sell out&lt;/li&gt;
&lt;li&gt;prices move&lt;/li&gt;
&lt;li&gt;collections get reorganized&lt;/li&gt;
&lt;li&gt;seasonal sale pages appear&lt;/li&gt;
&lt;li&gt;best-selling collections shift&lt;/li&gt;
&lt;li&gt;product tags reveal merchandising logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual monitoring misses those changes. A scheduled Actor run catches them.&lt;/p&gt;

&lt;p&gt;For a DTC brand, this matters because competitors rarely announce every test. A new bundle page, a 25% discount, or a "back in stock" product can explain shifts in paid search, email campaigns, or conversion performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advantage of catalog-level data
&lt;/h2&gt;

&lt;p&gt;Most price monitoring tools focus on product detail pages. That works when you already know which products matter. It fails when competitors launch new items.&lt;/p&gt;

&lt;p&gt;Catalog-level scraping lets you detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;new product handles&lt;/li&gt;
&lt;li&gt;removed products&lt;/li&gt;
&lt;li&gt;changed variants&lt;/li&gt;
&lt;li&gt;price changes&lt;/li&gt;
&lt;li&gt;sale status changes&lt;/li&gt;
&lt;li&gt;collection additions&lt;/li&gt;
&lt;li&gt;tag changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how you catch product strategy, not just pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sale tracking
&lt;/h2&gt;

&lt;p&gt;Shopify Scraper Pro derives sale information from price and compare-at price fields, giving you &lt;code&gt;onSale&lt;/code&gt;, &lt;code&gt;comparePrice&lt;/code&gt;, and &lt;code&gt;discountPercent&lt;/code&gt; style analysis.&lt;/p&gt;

&lt;p&gt;This is useful because many stores don't write "sale" in the product title. The discount exists in structured pricing data. Once the output is in a dataset, sort by discount percent and identify aggressive promotions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weekly diff
&lt;/h2&gt;

&lt;p&gt;The most useful part of the workflow is the diff between two runs. Compare the latest catalog against the previous snapshot and tag each product as new, removed, unchanged, price changed, variant changed, or sale changed.&lt;/p&gt;

&lt;p&gt;That diff gives the team a practical report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;products launched this week&lt;/li&gt;
&lt;li&gt;products removed from the catalog&lt;/li&gt;
&lt;li&gt;products newly discounted&lt;/li&gt;
&lt;li&gt;products with deeper discounts&lt;/li&gt;
&lt;li&gt;products back in stock&lt;/li&gt;
&lt;li&gt;products that sold out&lt;/li&gt;
&lt;li&gt;collections that changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is much easier to read than a full catalog export. A 2,000-product competitor catalog is not an insight. A list of 37 changed products is.&lt;/p&gt;

&lt;p&gt;Keep product handles as stable identifiers. Titles change for merchandising reasons, but handles are usually more stable. When handles disappear, that often means a product was removed or replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example workflow
&lt;/h2&gt;

&lt;p&gt;For a skincare brand, monitor five competitor stores and focus on the "new arrivals," "best sellers," and "sale" collections. The weekly report would show which products launched, which bundles appeared, and which items received discounts.&lt;/p&gt;

&lt;p&gt;For a fashion brand, variant availability matters more. If a competitor keeps selling out of medium sizes in a specific collection, that may indicate demand for a silhouette or color trend.&lt;/p&gt;

&lt;p&gt;For a supplements brand, tags and product types are useful. Competitors often expose product positioning through tags like sleep, recovery, focus, energy, or vegan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to add next
&lt;/h2&gt;

&lt;p&gt;The next layer is alerting. A Slack alert when a competitor launches a new product is useful, but a smarter alert is better: notify only when a new product appears in a tracked collection, has a discount above 20%, or matches a keyword such as bundle, subscription, starter kit, or limited edition.&lt;/p&gt;

&lt;p&gt;It's also worth connecting catalog changes to ad monitoring. If a new Shopify product appears and Facebook ads for the same product launch two days later, that tells a much clearer story than either dataset alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Run discovery and tracking separately.&lt;/strong&gt; First, crawl the whole store to understand catalog structure. Then monitor specific stores or collections on a schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch variants.&lt;/strong&gt; A product can stay live while important sizes or colors sell out. Variant availability often matters more than product availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track collections.&lt;/strong&gt; Collection membership reveals merchandising. A product moving into "best sellers" or "new arrivals" can be a stronger signal than a price change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalize currencies downstream.&lt;/strong&gt; If you track international Shopify stores, keep original currency fields and convert later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not scrape checkout or private customer data.&lt;/strong&gt; Public catalog monitoring is enough for competitive intelligence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SKU-level monitoring alone misses strategy.&lt;/strong&gt; Watching individual products catches price changes but misses strategy. Collection-level scraping shows launches, merchandising changes, and sale timing, which usually matters more than one product moving by two dollars.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual browsing&lt;/td&gt;
&lt;td&gt;Obvious price changes&lt;/td&gt;
&lt;td&gt;Misses launches and variants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price intelligence SaaS&lt;/td&gt;
&lt;td&gt;Known SKU monitoring&lt;/td&gt;
&lt;td&gt;Can be expensive and rigid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopify Scraper Pro on Apify&lt;/td&gt;
&lt;td&gt;Catalog, price, variant, collection changes&lt;/td&gt;
&lt;td&gt;Requires your own reporting layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check Shopify Scraper Pro's Actor page for the current pricing model before running at scale — Apify Actor pricing can change.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify scrape Shopify stores?
&lt;/h2&gt;

&lt;p&gt;Yes. Shopify Scraper Pro extracts structured public catalog data from Shopify-powered stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I monitor product price changes over time?
&lt;/h2&gt;

&lt;p&gt;Yes. Schedule runs and compare current prices against the previous dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I need login credentials?
&lt;/h2&gt;

&lt;p&gt;No. This workflow uses public storefront catalog data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Pick three competitor Shopify stores, run Shopify Scraper Pro with a small limit first, then export two weekly snapshots and diff them.&lt;/p&gt;

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

&lt;p&gt;Pick three competitor Shopify stores. Run &lt;a href="https://apify.com/crawlerbros/shopify-scraper-pro" rel="noopener noreferrer"&gt;Shopify Scraper Pro&lt;/a&gt; with a small limit first, then expand to the full catalog. Export two weekly snapshots. The changed rows will show you new products, discounts, availability shifts, and catalog strategy without another SaaS subscription.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>devops</category>
      <category>webdev</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>I had never read my own Actors the way an agent reads them</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 11 Sep 2026 05:18:24 +0000</pubDate>
      <link>https://dev.to/apify/i-had-never-read-my-own-actors-the-way-an-agent-reads-them-1h6d</link>
      <guid>https://dev.to/apify/i-had-never-read-my-own-actors-the-way-an-agent-reads-them-1h6d</guid>
      <description>&lt;p&gt;I have 23 Actors on the Apify Store. I have read their Console input forms hundreds of times - every&lt;br&gt;
field label, every hint, every checkbox, because I wrote them and then fixed them and then fixed them&lt;br&gt;
again.&lt;/p&gt;

&lt;p&gt;I had never once read the tool definition an AI agent receives when it calls them through the&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6r10gkpgbpllj23rrlon.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6r10gkpgbpllj23rrlon.jpg" alt="The Console input form for one of my Actors: two array fields, each pre-populated with the prefill values I wrote for human users - facebook/create-react-app, babel/babel-eslint, npm:request. This is the interface I reviewed dozens of times." width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is what I kept looking at. It is not what my callers see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last week I did. Of the 246 field descriptions I have written across those 23 Actors, &lt;strong&gt;53 arrive at&lt;br&gt;
the agent exactly as I typed them&lt;/strong&gt;. That is 21.5%. The rest are altered on the way out.&lt;/p&gt;

&lt;p&gt;Most of those alterations are documented, sensible, and probably improvements. That is not the point.&lt;br&gt;
The point is that I shipped 23 tools without ever looking at what my callers actually receive, and&lt;br&gt;
when I finally looked, I found three things the docs do not mention and one mistake that was entirely&lt;br&gt;
mine.&lt;/p&gt;

&lt;p&gt;Here is how to look, and what I found.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading your own tool definitions
&lt;/h2&gt;

&lt;p&gt;The Apify MCP server speaks Streamable HTTP, the transport defined by the&lt;br&gt;
&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;. You post JSON-RPC to it, you get&lt;br&gt;
&lt;code&gt;tools/list&lt;/code&gt; back, and that response is the ground truth for what an agent sees.&lt;/p&gt;

&lt;p&gt;The first trap costs a minute: &lt;strong&gt;the endpoint is &lt;code&gt;/&lt;/code&gt;, not &lt;code&gt;/mcp&lt;/code&gt;.&lt;/strong&gt; Post to &lt;code&gt;/mcp&lt;/code&gt; and the server&lt;br&gt;
tells you off in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is nothing at route POST /mcp?actors=... This Model Context Protocol (MCP) server supports the Streamable HTTP transport.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second trap costs a little more: &lt;code&gt;notifications/initialized&lt;/code&gt; answers with an empty body, so a&lt;br&gt;
naive &lt;code&gt;JSON.parse&lt;/code&gt; on every response throws. I wrote that bug while writing this article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read your own Actors the way an AI agent reads them.&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: APIFY_TOKEN=... node read-my-tools.mjs aiqlabs/sitemap-checker aiqlabs/pdf-inspector&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://mcp.apify.com/?token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;actors=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actors&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json, text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// notifications answer 202 with no body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initialize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;protocolVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-11-05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;clientInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read-my-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications/initialized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at three of mine - say &lt;a href="https://apify.com/aiqlabs/sitemap-checker" rel="noopener noreferrer"&gt;&lt;code&gt;sitemap-checker&lt;/code&gt;&lt;/a&gt; and two&lt;br&gt;
others - and it prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aiqlabs--github-repository-audit  (490 chars, 16 params)
  injected by the server : waitSecs
  prefill left in schema : repos, packages
  truncated at 500 chars : repos

aiqlabs--sitemap-checker  (421 chars, 12 params)
  injected by the server : waitSecs
  prefill left in schema : domains
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of output, three things worth knowing. Take them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget nobody tells you about
&lt;/h2&gt;

&lt;p&gt;The description an agent reads is not your description. It is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This tool calls the Actor "&amp;lt;user&amp;gt;/&amp;lt;slug&amp;gt;" and retrieves its output results.
Use this tool instead of the "call-actor" if user requests this specific Actor.
Actor description: &amp;lt;your description&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README states that descriptions are truncated at 500 characters (&lt;code&gt;MAX_DESCRIPTION_LENGTH&lt;/code&gt;). That&lt;br&gt;
limit applies to the whole string above, preamble included - so what you actually get is 500 minus&lt;br&gt;
the preamble.&lt;/p&gt;

&lt;p&gt;And the preamble is not a fixed cost. Your Actor's full name sits inside it, which means &lt;strong&gt;a longer&lt;br&gt;
slug buys you a shorter description&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;budget = 338 − length("&amp;lt;user&amp;gt;/&amp;lt;slug&amp;gt;")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across my 23 that works out to &lt;strong&gt;303 to 317 characters&lt;/strong&gt;. &lt;code&gt;domain-availability-checker&lt;/code&gt; - my longest&lt;br&gt;
slug at 35 characters - gets 303. &lt;code&gt;pdf-inspector&lt;/code&gt; gets 317. Nothing anywhere told me that naming an&lt;br&gt;
Actor spends part of its description.&lt;/p&gt;

&lt;p&gt;None of mine are over. But the margins are thinner than I would have guessed: my tightest is&lt;br&gt;
&lt;code&gt;github-repository-audit&lt;/code&gt; at 10 characters of headroom, then &lt;code&gt;hacker-news-link-rot&lt;/code&gt; at 11 and&lt;br&gt;
&lt;code&gt;chrome-extension-audit&lt;/code&gt; at 12. Median description across the 23 is 278. Three of them are one&lt;br&gt;
average sentence away from being delivered with the last clause replaced by &lt;code&gt;...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I found the real numbers late, and only because I checked. My first draft of this section said the&lt;br&gt;
preamble was "174 characters, fixed" and the budget was 326 - measured off a single Actor, which&lt;br&gt;
happened to be one of the longest-named ones, and then stated as a constant.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the server changes, in numbers
&lt;/h2&gt;

&lt;p&gt;Across 23 Actors and 246 author-written fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Fields&lt;/th&gt;
&lt;th&gt;Documented&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Example values: ...&lt;/code&gt; appended&lt;/td&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;items.description&lt;/code&gt; generated where I wrote none&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prefill&lt;/code&gt; kept in the exposed JSON Schema&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;waitSecs&lt;/code&gt; parameter added&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;**REQUIRED**&lt;/code&gt; prefix added&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Possible values: ...&lt;/code&gt; appended&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description truncated at 500 chars&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero of my 246 fields had an empty description, so none of this is blanks being filled in. It is text&lt;br&gt;
I wrote being changed.&lt;/p&gt;

&lt;p&gt;The documented half is in the &lt;a href="https://github.com/apify/actors-mcp-server" rel="noopener noreferrer"&gt;server's README&lt;/a&gt; and it is&lt;br&gt;
worth reading once:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions&lt;/strong&gt; are truncated to 500 characters (as defined in &lt;code&gt;MAX_DESCRIPTION_LENGTH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required fields&lt;/strong&gt; are explicitly marked with a &lt;code&gt;REQUIRED&lt;/code&gt; prefix in their descriptions for compatibility with frameworks that may not handle the JSON schema properly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;I checked the second claim rather than trusting it: 17 fields are declared &lt;code&gt;required&lt;/code&gt; in my schemas,&lt;br&gt;
and &lt;strong&gt;all 17&lt;/strong&gt; carry the prefix in the tool definition. It does what it says.&lt;/p&gt;
&lt;h2&gt;
  
  
  The truncation that undoes itself
&lt;/h2&gt;

&lt;p&gt;Exactly one of my 246 fields is longer than 500 characters: the &lt;code&gt;repos&lt;/code&gt; field on&lt;br&gt;
&lt;a href="https://apify.com/aiqlabs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;github-repository-audit&lt;/code&gt;&lt;/a&gt;, at 624. Its top-level description arrives cut, with the last 124 characters&lt;br&gt;
replaced by &lt;code&gt;...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those 124 characters read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;...they could not be checked, not because they came back clean. Give the package name in Packages instead if you need those three.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not decoration. That sentence exists to stop a caller reading a &lt;code&gt;null&lt;/code&gt; as a clean result - the&lt;br&gt;
single misreading this Actor was built to prevent. Truncation ate exactly the warning.&lt;/p&gt;

&lt;p&gt;Except the agent still gets it. The server had also generated an &lt;code&gt;items.description&lt;/code&gt; for that array&lt;br&gt;
field - something I never wrote - containing the &lt;strong&gt;full 624 characters, byte for byte&lt;/strong&gt;. A documented&lt;br&gt;
truncation was quietly cancelled by an undocumented generation.&lt;/p&gt;

&lt;p&gt;I want to be precise about how much this shows. &lt;strong&gt;One field in 246 was long enough to test it.&lt;/strong&gt; I am&lt;br&gt;
reporting a mechanism, not a rate. But the mechanism is worth knowing in both directions: if your long&lt;br&gt;
description sits on an &lt;code&gt;array&lt;/code&gt;, the full text survives in &lt;code&gt;items&lt;/code&gt;; if it sits on a &lt;code&gt;string&lt;/code&gt;, nothing&lt;br&gt;
catches it.&lt;/p&gt;

&lt;p&gt;Where does &lt;code&gt;items.description&lt;/code&gt; come from? Of my 39 array fields, 36 got one. Twenty-four are exact&lt;br&gt;
copies of the parent description, eleven are the parent plus the &lt;code&gt;**REQUIRED**&lt;/code&gt; prefix, and one is a&lt;br&gt;
structural expansion of &lt;code&gt;editor: requestListSources&lt;/code&gt; into its &lt;code&gt;url&lt;/code&gt; / &lt;code&gt;method&lt;/code&gt; / &lt;code&gt;payload&lt;/code&gt; / &lt;code&gt;headers&lt;/code&gt;&lt;br&gt;
shape. The three that got nothing all have an &lt;code&gt;enum&lt;/code&gt; on their items.&lt;/p&gt;

&lt;p&gt;That last case caught me out while I was measuring. I first recorded it as "my 138-character&lt;br&gt;
description was replaced by a 19-character string" and nearly published that. It was wrong: the parent&lt;br&gt;
description arrives intact, and the 19 characters are the title of the expanded object type. Nothing&lt;br&gt;
was lost. I mention it because it is the kind of error that reads perfectly well in a draft.&lt;/p&gt;
&lt;h2&gt;
  
  
  The parameter you did not write
&lt;/h2&gt;

&lt;p&gt;Every one of my 23 tools carries a parameter I have never declared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Max seconds (0–45, default 30) to cap the wait for the Actor run to reach terminal state. For long-running Actors the response returns at the cap with the current run status; follow `nextStep` to poll via get-actor-run. Set to 0 to fire-and-forget."&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;It is on the server's own tools too - &lt;code&gt;get-actor-run&lt;/code&gt; carries it - so this is a server-wide&lt;br&gt;
convention rather than something done to Actors specifically.&lt;/p&gt;

&lt;p&gt;I am not reporting this as a defect. The description tells the agent precisely how to recover, which&lt;br&gt;
is more than most timeouts do. I am reporting it because &lt;strong&gt;an author who has never read their tool&lt;br&gt;
definition does not know their Actor now has a 45-second contract with its callers.&lt;/strong&gt; If your Actor&lt;br&gt;
takes four minutes on a realistic input, every agent call returns before it finishes and the caller's&lt;br&gt;
experience depends on whether their client follows &lt;code&gt;nextStep&lt;/code&gt;. That is a design constraint on your&lt;br&gt;
Actor, and it arrived without you.&lt;/p&gt;

&lt;h2&gt;
  
  
  One key, two layers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prefill&lt;/code&gt; survives into the exposed JSON Schema on 33 of my fields. It is not a&lt;br&gt;
&lt;a href="https://json-schema.org/understanding-json-schema/reference/array" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt; keyword; nothing in&lt;br&gt;
the spec says what a consumer should do with it. An agent reading the schema sees a value&lt;br&gt;
sitting next to my field and can reasonably read it as a suggestion.&lt;/p&gt;

&lt;p&gt;I have met this key before. In an earlier article I traced a run that returned four rows when I had&lt;br&gt;
asked about one repository: the platform was treating my &lt;code&gt;prefill&lt;/code&gt; as a &lt;code&gt;default&lt;/code&gt; and injecting it&lt;br&gt;
into every call that omitted the field. Same key, different layer, different behaviour - and neither&lt;br&gt;
behaviour is one I asked for when I filled in a Console form hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I let an agent choose between all 23
&lt;/h2&gt;

&lt;p&gt;Reading the definitions tells you what arrives. It does not tell you whether the catalog works. So I&lt;br&gt;
ran a second trial: give an agent all 23 descriptions and one realistic task, and see which tool it&lt;br&gt;
reaches for.&lt;/p&gt;

&lt;p&gt;I wrote seven tasks, fixed the intended answer for each one in a file &lt;strong&gt;before launching anything&lt;/strong&gt;,&lt;br&gt;
and wrote down what I expected to happen. Two independent agents per task, fourteen trials, no Actor&lt;br&gt;
executed.&lt;/p&gt;

&lt;p&gt;I expected failures. My catalog has obvious traps in it - three PDF tools, three tools that all touch&lt;br&gt;
broken links, two that both check domains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fourteen out of fourteen picked the tool I intended.&lt;/strong&gt; Three of my seven predictions were wrong, and&lt;br&gt;
every one of them was wrong in the same direction: the agents were more careful than I gave them&lt;br&gt;
credit for. Task 7 is a fair example. I expected &lt;code&gt;http-status-checker&lt;/code&gt; to pull votes away from&lt;br&gt;
&lt;code&gt;dead-link-checker&lt;/code&gt;; instead both agents explained, unprompted, that a status checker can only test&lt;br&gt;
URLs you already have and therefore cannot recover the URLs of a blog that moved two years ago.&lt;/p&gt;

&lt;p&gt;So the selection layer was not the story. This was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence fell to &lt;code&gt;medium&lt;/code&gt; in exactly 4 of the 14 trials, and a tie was declared in exactly those&lt;br&gt;
same 4.&lt;/strong&gt; The other ten were unanimous, &lt;code&gt;high&lt;/code&gt;, and tie-free. Those four trials are the two pairs&lt;br&gt;
where I had written overlapping descriptions.&lt;/p&gt;

&lt;p&gt;Here is one of them, in my own words, from the live catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt; - "classic SEO health and for &lt;strong&gt;AI/LLM discoverability - AI crawler access in robots.txt, llms.txt&lt;/strong&gt;, structured data, metadata"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seo-audit-tool&lt;/code&gt; - "on-page and technical SEO, and adds four checks general auditors skip: &lt;strong&gt;AI crawler access in robots.txt, llms.txt&lt;/strong&gt;, redirects that drop the path, and noindex sent via X-Robots-Tag"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The phrase "AI crawler access in robots.txt, llms.txt" appears verbatim in both. I wrote both&lt;br&gt;
descriptions, months apart, and had never once read them next to each other.&lt;/p&gt;

&lt;p&gt;Asked "is my robots.txt blocking AI crawlers like GPTBot", both agents picked&lt;br&gt;
&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt;, and both told me why the pick was thin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;both entries explicitly list "AI crawler access in robots.txt" as a check, so either would answer&lt;br&gt;
the question; I picked seo-ai-visibility-auditor because AI/LLM discoverability is its stated&lt;br&gt;
primary purpose rather than one of four add-on checks. Neither description names GPTBot&lt;br&gt;
specifically, so the match rests on the "AI crawler access in robots.txt" wording alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a correct answer arrived at by elimination, with the reasoning shown and the weakness&lt;br&gt;
declared. The agent did the honest thing. The duplication it had to route around is mine.&lt;/p&gt;

&lt;p&gt;What I wanted to conclude here was: &lt;strong&gt;the signal is the confidence, not the choice&lt;/strong&gt; - every &lt;code&gt;medium&lt;/code&gt;&lt;br&gt;
landed on a real duplication, so watch where the agent hesitates. I had the sentence written. Then I&lt;br&gt;
measured it and had to take it out. That is the next section.&lt;/p&gt;

&lt;p&gt;Two caveats regardless. &lt;strong&gt;n is 2 per task&lt;/strong&gt;, all Claude, all in one harness - this is a check on my&lt;br&gt;
catalog, not evidence about agents in general. And the agents saw name and description only; a real&lt;br&gt;
client also reads the input schema, which might break a tie my prose leaves open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it, and measuring the fix the same way
&lt;/h2&gt;

&lt;p&gt;The two SEO Actors were never duplicates in the code. One takes a starting URL and crawls&lt;br&gt;
(&lt;code&gt;startUrls&lt;/code&gt;, &lt;code&gt;crawlSite&lt;/code&gt;, &lt;code&gt;maxPages&lt;/code&gt;); the other takes a list and audits it page by page (&lt;code&gt;urls&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;maxUrls&lt;/code&gt;). They are genuinely different tools. My descriptions simply never said so - they both led&lt;br&gt;
with the checks, and the checks overlap.&lt;/p&gt;

&lt;p&gt;So I rewrote both to lead with the input shape, and to name each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt; - "...&lt;strong&gt;Give it a domain to crawl&lt;/strong&gt; - for a fixed list of pages, use SEO Audit Tool instead."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seo-audit-tool&lt;/code&gt; - "&lt;strong&gt;Give it a list of URLs&lt;/strong&gt;... One row per URL - to crawl a whole site instead, use SEO &amp;amp; AI Visibility Auditor."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I pulled &lt;code&gt;tools/list&lt;/code&gt; again to confirm the agent-facing text had actually changed - no tool in&lt;br&gt;
the catalog still carries the shared phrase - and ran the same task past three fresh agents. All&lt;br&gt;
three: &lt;code&gt;high&lt;/code&gt;, no tie. One explained it without being asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;aiqlabs/seo-audit-tool also covers "AI crawler rules" but is scoped to a supplied list of URLs, and&lt;br&gt;
both descriptions explicitly cross-reference each other to resolve that split, so this is not a tie.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clean result. I nearly stopped there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The control that ruined it
&lt;/h3&gt;

&lt;p&gt;I also re-ran task 4 - the domain pair - as a control. &lt;strong&gt;I had not touched either of those two&lt;br&gt;
descriptions.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pair&lt;/th&gt;
&lt;th&gt;edited?&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;✅ yes&lt;/td&gt;
&lt;td&gt;medium ×2, tie ×2&lt;/td&gt;
&lt;td&gt;high ×3, tie none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;domain&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;no&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;medium ×2, tie ×2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;high ×2, tie none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pair I did not fix improved exactly as much as the pair I did. So the confidence change cannot be&lt;br&gt;
attributed to my rewrite, and the sentence I wanted to publish - &lt;em&gt;watch where the agent hesitates&lt;/em&gt; - &lt;br&gt;
does not survive its own control.&lt;/p&gt;

&lt;p&gt;The cause is a flaw in my design, not a mystery. My before and after prompts were not identical. The&lt;br&gt;
before agents were told "answer in exactly this structure and nothing else"; they produced answers and&lt;br&gt;
then sat on them, and I had to chase them with a second message asking them to actually send it. The&lt;br&gt;
after agents had the delivery instruction from the start. One changed variable, sitting right next to&lt;br&gt;
the one I was trying to measure.&lt;/p&gt;

&lt;p&gt;What survives is narrower and still worth having. The duplication was real: the same clause, verbatim,&lt;br&gt;
in two live descriptions I wrote months apart. Four of fourteen before-trials declared a tie and &lt;strong&gt;all&lt;br&gt;
four landed on the two pairs where my descriptions genuinely overlapped&lt;/strong&gt; - no false alarms among the&lt;br&gt;
other ten. The fix shipped, and the shared phrase is gone from what agents receive. What I cannot tell&lt;br&gt;
you is whether fixing it changed anything measurable, because I broke my own instrument while using it.&lt;/p&gt;

&lt;p&gt;If I had measured only the pair I edited - which was my plan until I added the control as an&lt;br&gt;
afterthought - I would have published a causal claim resting on a changed prompt.&lt;/p&gt;

&lt;p&gt;One more honest note on that edit. While writing the new description I listed "soft 404s answering 200" as&lt;br&gt;
one of the four checks. The Actor does detect those - there is a &lt;code&gt;soft_404&lt;/code&gt; code in its source - but&lt;br&gt;
my own README defines the four as AI crawler rules, &lt;code&gt;/llms.txt&lt;/code&gt;, path-dropping redirects, and&lt;br&gt;
&lt;code&gt;X-Robots-Tag&lt;/code&gt; noindex. I had quietly swapped one out. I caught it by reading the README before&lt;br&gt;
publishing rather than after, which is the only reason it is a footnote instead of a correction.&lt;/p&gt;

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

&lt;p&gt;Four things, none of them large:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I compute the budget per Actor&lt;/strong&gt; - &lt;code&gt;338 − length("&amp;lt;user&amp;gt;/&amp;lt;slug&amp;gt;")&lt;/code&gt; - instead of assuming one
number. Three of mine sit within 12 characters of losing a sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No field description over 500 characters unless the field is an &lt;code&gt;array&lt;/code&gt;.&lt;/strong&gt; On an array, &lt;code&gt;items&lt;/code&gt;
catches the overflow. On a string, it does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools/list&lt;/code&gt; before publish.&lt;/strong&gt; It takes ten seconds and it is the only view of your Actor that
your non-human callers actually have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read your descriptions as a set, not one at a time.&lt;/strong&gt; This is the one that actually cost me
something. I wrote two SEO Actors months apart, gave them the same clause, and never once put the
two sentences side by side. A catalog is a document; mine was 23 documents that had never met.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The wider point is smaller than a bug and more annoying than one. I built 23 tools for AI agents to&lt;br&gt;
call and spent all of my review time in a form built for humans. The one interface my actual callers&lt;br&gt;
use, I had never opened.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>agents</category>
      <category>webdev</category>
    </item>
    <item>
      <title>prefill and default look the same in Console. Only one of them lets an agent skip your required field.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 11 Sep 2026 05:18:13 +0000</pubDate>
      <link>https://dev.to/apify/prefill-and-default-look-the-same-in-console-only-one-of-them-lets-an-agent-skip-your-required-4bc</link>
      <guid>https://dev.to/apify/prefill-and-default-look-the-same-in-console-only-one-of-them-lets-an-agent-skip-your-required-4bc</guid>
      <description>&lt;p&gt;I have 23 audit Actors on the &lt;a href="https://apify.com/store" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. They all do a version of the same thing: take a public record, check whether what it still claims is true, and write the verdict into a dataset.&lt;/p&gt;

&lt;p&gt;Last week I set out to prove they were unsafe for AI agents to chain together. I had a specific accusation in mind, I built the experiment to demonstrate it, and the experiment refused. What I found instead was a one-line schema fix that I have now applied, and a distinction between two input-schema settings that I had treated as interchangeable for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accusation I was going to make
&lt;/h2&gt;

&lt;p&gt;Chaining means running Actor A, taking something out of its output, and passing it into Actor B. For an agent to do that through the &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;, B has to be callable with a value that only A can supply. MCP is the Model Context Protocol, the interface that exposes your Actor to clients like Claude and Cursor as a tool they can call on their own.&lt;/p&gt;

&lt;p&gt;So I read my own catalogue with a script instead of from memory. Of my 23 published Actors, &lt;strong&gt;9&lt;/strong&gt; declare a URL or a domain in their input schema's &lt;code&gt;required&lt;/code&gt; array. Counting every ordered pair where A emits the kind of identifier B requires, I get &lt;strong&gt;61&lt;/strong&gt; possible chains: 36 joined by a URL, 25 by a domain.&lt;/p&gt;

&lt;p&gt;A tenth Actor requires an identifier too, and it is the interesting one. &lt;code&gt;dataset-to-github-issues&lt;/code&gt; takes a dataset ID and a repository name. Nothing else in my catalogue emits either, so no agent can ever reach it from another Actor's output - it is a second stage with no possible first stage. I had not noticed that until the script told me.&lt;/p&gt;

&lt;p&gt;Then I looked at what those 9 do when the value never arrives.&lt;/p&gt;

&lt;p&gt;I had swept my catalogue for target-naming defaults once before and found them, so the count itself was not the surprise. Restricting it to the Actors that can actually be a second stage sharpens it: &lt;strong&gt;seven of the nine carry a &lt;code&gt;default&lt;/code&gt; that names a concrete target.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulk-domain-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dead-link-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["docs.apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain-availability-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http-status-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["https://apify.com/store"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;startUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"url":"https://apify.com"}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tech-stack-detector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All seven are also listed in &lt;code&gt;required&lt;/code&gt;. I had assumed that word did some work.&lt;/p&gt;

&lt;p&gt;My accusation was going to be this: in a two-stage workflow, an agent that drops the value between stages gets no error. The second Actor runs on &lt;code&gt;apify.com&lt;/code&gt;, returns a well-formed dataset, and the agent reports a confident answer about a website the user never asked about. A wrong answer with no failure attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the part that turned out to be true
&lt;/h2&gt;

&lt;p&gt;Before involving any agent, I called two of my own Actors with an empty input from the command line.&lt;/p&gt;

&lt;p&gt;The first, &lt;a href="https://apify.com/aiqlabs/seo-audit-tool" rel="noopener noreferrer"&gt;&lt;code&gt;seo-audit-tool&lt;/code&gt;&lt;/a&gt;, has &lt;code&gt;urls&lt;/code&gt; in &lt;code&gt;required&lt;/code&gt; and &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apify call aiqlabs/seo-audit-tool &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run: Calling Actor aiqlabs/seo-audit-tool (ZjU3YtyaWgqpNriNj)

Error: Input is not valid: Field input.urls is required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No run was created. No dataset, no compute, no partial result to misread. The platform named the missing field and stopped.&lt;/p&gt;

&lt;p&gt;The second, &lt;a href="https://apify.com/aiqlabs/http-status-checker" rel="noopener noreferrer"&gt;&lt;code&gt;http-status-checker&lt;/code&gt;&lt;/a&gt;, has &lt;code&gt;urls&lt;/code&gt; in &lt;code&gt;required&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; a &lt;code&gt;default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apify call aiqlabs/http-status-checker &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Checking 1 URL(s) with 8 in flight. HTML analysis: on.
INFO  Done. 1 URL(s): 0 error, 0 warning, 0 info, 1 clean.

Apify call result: SUCCEEDED
Run ID: sMLok9a5KMSSdtnD1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I sent nothing. It audited something. Reading the input the platform stored for that run shows what:&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;"urls"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"analyzeHtml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"respectRobotsTxt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"robotsAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Googlebot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"onlyIssues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"requestTimeoutSecs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxRedirects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"slowResponseMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10000&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;Same platform, same account, same minute, same field name, both fields marked &lt;code&gt;required&lt;/code&gt;. One call was refused and one was silently completed. The only difference is the &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a second thing in that pair worth pulling out. &lt;code&gt;seo-audit-tool&lt;/code&gt; does carry a &lt;code&gt;prefill&lt;/code&gt; on &lt;code&gt;urls&lt;/code&gt; - two example URLs that appear in the Console form. It was still rejected. &lt;strong&gt;&lt;code&gt;prefill&lt;/code&gt; populates the form for a human; &lt;code&gt;default&lt;/code&gt; is substituted for whoever omits the field.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema/specification/v1" rel="noopener noreferrer"&gt;input schema specification&lt;/a&gt; is not vague about this. On &lt;code&gt;prefill&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;this field is only used in the user interface but does not affect the Actor functionality and API&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and, in the same paragraph:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the Prefill value won't be used by existing integrations such as Actor tasks or API calls, but the Default will be if specified&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On &lt;code&gt;default&lt;/code&gt;, it names the callers explicitly: the platform passes the value when the user omits it "via any means (API, CLI, scheduler, or user interface)". An agent calling through the MCP server is one of those means.&lt;/p&gt;

&lt;p&gt;I had read that page. I read it as a description of two features and used both, on the same field, for months.&lt;/p&gt;

&lt;p&gt;There is a detail in &lt;code&gt;http-status-checker&lt;/code&gt; that makes the gap concrete. The two settings on &lt;code&gt;urls&lt;/code&gt; do not even hold the same value:&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="nl"&gt;"urls"&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;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&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://apify.com/this-page-does-not-exist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"http://apify.com/"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&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;Three URLs for the form, one URL for anyone who omits the field. I wrote both, months apart, and never put them side by side. A caller that skips &lt;code&gt;urls&lt;/code&gt; does not get the demo I built - it gets a shorter thing I stopped thinking about, and no part of the interface ever shows the two together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the accusation collapsed
&lt;/h2&gt;

&lt;p&gt;To test the chain I needed a question that one Actor cannot answer on its own. That took two attempts.&lt;/p&gt;

&lt;p&gt;My first idea was &lt;code&gt;sitemap-checker&lt;/code&gt; into &lt;code&gt;http-status-checker&lt;/code&gt;: enumerate a site's sitemap, then check the URLs. It is a bad example, because &lt;code&gt;sitemap-checker&lt;/code&gt; has &lt;code&gt;checkUrlStatus&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt; by default and already requests every URL. My second idea was &lt;code&gt;hacker-news-link-rot&lt;/code&gt; into &lt;code&gt;domain-availability-checker&lt;/code&gt;, and that fails the same way - the first Actor already emits &lt;code&gt;domainRegistrationStatus&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is worth pausing on. I had built every one of these Actors to answer a question completely, which is right for a standalone Store listing and is exactly what makes them poor chain stages. Composable and self-contained are not the same design, and I had only ever aimed at one of them.&lt;/p&gt;

&lt;p&gt;The question I settled on works because it needs HTML analysis, which the sitemap Actor does not do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Among the URLs listed in crawlee.dev's sitemap, which ones are marked noindex, and which ones are soft 404s?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A &lt;a href="https://developers.google.com/search/docs/crawling-indexing/block-indexing" rel="noopener noreferrer"&gt;noindex&lt;/a&gt; page is one that tells search engines to keep it out of the index; a &lt;a href="https://developers.google.com/search/docs/crawling-indexing/http-network-errors" rel="noopener noreferrer"&gt;soft 404&lt;/a&gt; is a missing page that returns 200 anyway. Both need the HTML of the page. &lt;code&gt;sitemap-checker&lt;/code&gt; emits 11 fields and none of them is either. &lt;code&gt;http-status-checker&lt;/code&gt; has both, behind an &lt;code&gt;analyzeHtml&lt;/code&gt; switch that is on by default. The chain is necessary, and &lt;a href="https://crawlee.dev" rel="noopener noreferrer"&gt;crawlee.dev&lt;/a&gt; is a real documentation site with a large sitemap, which makes it a fair test.&lt;/p&gt;

&lt;p&gt;I gave that question, word for word, to four fresh agents with both Actors exposed through the Apify MCP server, and no other instructions. I did not tell them to be careful, and I did not mention inputs or defaults - in an earlier experiment I learned that asking an agent to count its rows is the same as telling it where to look.&lt;/p&gt;

&lt;p&gt;Then I ignored what they told me and read what the platform stored.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stage-2 run&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;urls&lt;/code&gt; passed&lt;/th&gt;
&lt;th&gt;first entry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p5qwOESMQB0Vwj0ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.11/playwright-crawler/...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NPM7mW5oC848wn2ZL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;174&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.16/puppeteer-crawler/...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3fO6vTbKnLlkDwGnw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/blog&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;H0HkakmGE2bfkAdWU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.15/core/interface/HttpResponse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2cJHKXtsrapw9aikV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.14/browser-pool/class/PuppeteerPlugin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oGbGCrfPEM4uDuOBY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.12/puppeteer-crawler&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sDQnaUb7NxX9iz88B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.11/core/class/SystemStatus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QDpb6PG685ptVthUN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/blog&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bB20ynAk9ymZafaiY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;724&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.14/utils/function/downloadListOfUrls&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nine stage-2 runs. Every one carries real crawlee.dev URLs. Not one carries &lt;code&gt;https://apify.com/store&lt;/code&gt;. They also batched - 174 to 724 URLs per call - rather than making one call per URL.&lt;/p&gt;

&lt;p&gt;The trap I had spent a day setting never closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it never closed
&lt;/h2&gt;

&lt;p&gt;The mechanism is in the result once you stop arguing with it. A &lt;code&gt;default&lt;/code&gt; stands in for a value the caller never had. Inside a chain, the agent produced that value itself one step earlier and is still holding it. The omission the default is waiting for does not occur.&lt;/p&gt;

&lt;p&gt;That reconciles this with the case where the same defect does bite. When an agent calls one of these Actors cold - no preceding step, nothing in hand - the missing field is real, the platform fills it, and the run proceeds on a target nobody chose. The danger lives at the entry point of a workflow, not in the joins.&lt;/p&gt;

&lt;p&gt;I would rather publish that than the article I planned, because it changes where you should spend attention. If you are worried about agents mangling your Actor, the risk is concentrated in the first call, not in the handoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is wrong with my own experiment
&lt;/h2&gt;

&lt;p&gt;A reviewer would find this, so I will say it first.&lt;/p&gt;

&lt;p&gt;The question I picked has a null answer. Here is the summary record from one 500-URL batch:&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;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"broken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"soft404s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"noindexOnLivePages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"metaRefreshRedirects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;crawlee.dev has no soft 404s and no noindex pages in those URLs. "Which ones are noindex?" answers to "none". An agent that had quietly audited &lt;code&gt;apify.com/store&lt;/code&gt; instead would probably also have answered "none".&lt;/p&gt;

&lt;p&gt;So the agents' answers cannot tell a careful run from a careless one here. Only the stored inputs can. That is the one thing I got right by accident of habit: I built the measurement on records the platform keeps, not on what the agents said about themselves. It mattered more than I expected - &lt;strong&gt;all four agents finished without returning a report to me at all&lt;/strong&gt;, and every number above survived that because none of them came from an agent.&lt;/p&gt;

&lt;p&gt;A stronger version of this trial would use a site with known noindex pages, so a substituted target produces a &lt;em&gt;different&lt;/em&gt; answer rather than the same one. If you repeat this, pick your target site for that property.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and what it costs
&lt;/h2&gt;

&lt;p&gt;For any input field that names &lt;em&gt;what&lt;/em&gt; to work on - a URL, a domain, a repository, an account - delete the &lt;code&gt;default&lt;/code&gt; and keep the &lt;code&gt;prefill&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URLs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stringList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;"required": ["urls"]&lt;/code&gt; alongside it. The Console form still opens with an example filled in, so nothing changes for a human clicking through your Actor. What changes is that a caller who omits the field now gets &lt;code&gt;Field input.urls is required&lt;/code&gt; instead of a clean dataset about somebody else's website.&lt;/p&gt;

&lt;p&gt;The specification anticipated this too. Its guidance for &lt;code&gt;required&lt;/code&gt; reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use for fields that don't have a reasonable default and MUST be entered by the user (e.g., API token, password)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fields that don't have a reasonable default. Seven of mine had both, which is a combination the sentence quietly rules out. Marking a field &lt;code&gt;required&lt;/code&gt; and then supplying the answer yourself is not a stricter setting; it is two instructions that cancel.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;default&lt;/code&gt; for settings that describe &lt;em&gt;how&lt;/em&gt; to work - concurrency, timeouts, depth limits, feature switches. Those are genuinely safe to assume, and an empty call that inherits them is behaving correctly.&lt;/p&gt;

&lt;p&gt;The distinction is worth stating as a rule: &lt;strong&gt;&lt;code&gt;default&lt;/code&gt; on a method is a convenience; &lt;code&gt;default&lt;/code&gt; on a subject is a substitution.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking your own Actors
&lt;/h2&gt;

&lt;p&gt;I wrote the catalogue sweep as a script so I would stop trusting my memory of my own schemas. It reads every local &lt;code&gt;.actor/&lt;/code&gt; directory, classifies each input and output field into an identifier kind, and reports which of your Actors can be a chain stage and which of them will accept an empty call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node composability.mjs           &lt;span class="c"&gt;# summary and pair matrix&lt;/span&gt;
node composability.mjs &lt;span class="nt"&gt;--rules&lt;/span&gt;   &lt;span class="c"&gt;# print the classifier so you can audit it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--rules&lt;/code&gt; flag exists because the classifier is regular expressions over field names, and a regex table you cannot see is a number you should not trust. Mine misfired the first time I ran it: it reported that &lt;code&gt;github-repository-audit&lt;/code&gt; requires no identifier, which is nonsense for an Actor that audits repositories. The cause was real and boring - that schema has no &lt;code&gt;required&lt;/code&gt; key at all, so every field is optional and the defaults decide everything.&lt;/p&gt;

&lt;p&gt;If you want a single check with no script, call your own Actor with an empty input and see what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apify call &amp;lt;your-actor&amp;gt; &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it succeeds, look at what it audited. That is what an agent gets when it forgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed and what I did not
&lt;/h2&gt;

&lt;p&gt;I have not stripped the defaults from all seven Actors yet. The measurement says the harm is concentrated in cold calls, and I want to measure that path on these specific Actors before I change seven live listings - the last time I acted on a confident prediction here, the prediction was wrong twice.&lt;/p&gt;

&lt;p&gt;What I have changed is the rule I apply to new fields, and the check I run before publishing. Both of them came out of an experiment that failed to prove its own thesis, which is becoming a habit I am not entirely unhappy about.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Local market intelligence: Google Maps, reviews, and news with Apify</title>
      <dc:creator>Crawler Bros</dc:creator>
      <pubDate>Thu, 10 Sep 2026 06:30:03 +0000</pubDate>
      <link>https://dev.to/apify/local-market-intelligence-google-maps-reviews-and-news-with-apify-48ee</link>
      <guid>https://dev.to/apify/local-market-intelligence-google-maps-reviews-and-news-with-apify-48ee</guid>
      <description>&lt;h2&gt;
  
  
  Local market intelligence: Google Maps, reviews, and news with Apify
&lt;/h2&gt;

&lt;p&gt;Local competitors leave useful traces everywhere: map rankings, review velocity, star ratings, local press, opening hours, categories, photos, and website links.&lt;/p&gt;

&lt;p&gt;The problem is that those signals rarely live in one clean report. When a client asks, "Which dental clinics are gaining ground in Austin?", the answer is scattered across Google Maps, review pages, news results, and local search results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local market intelligence&lt;/strong&gt; is the practice of pulling those scattered public signals — Google Maps listings, customer reviews, and local press coverage — into one structured, weekly dataset an agency, franchise operator, or founder can use for decisions, instead of piecing it together from browser tabs and screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;You can build a local market intelligence pipeline on Apify by combining a Google Maps scraper for business discovery, a reviews scraper for customer sentiment, and a Google News scraper for local press mentions. CrawlerBros Actors return structured datasets that can be exported to Google Sheets, BI tools, or an internal CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The local intelligence stack
&lt;/h2&gt;

&lt;p&gt;The stack combines three CrawlerBros Actors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/google-maps-scraper" rel="noopener noreferrer"&gt;Google Maps Scraper&lt;/a&gt;&lt;/strong&gt; for business discovery, ratings, locations, categories, phone numbers, websites, and coordinates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/google-maps-reviews-scraper" rel="noopener noreferrer"&gt;Google Maps Reviews Scraper&lt;/a&gt;&lt;/strong&gt; for review text, ratings, dates, reviewer metadata, and owner response tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/crawlerbros/google-news-scraper" rel="noopener noreferrer"&gt;Google News Scraper&lt;/a&gt;&lt;/strong&gt; for brand mentions, competitor mentions, article snippets, full-text extraction, and publication metadata.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdiz0p9r8268kk8298va.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdiz0p9r8268kk8298va.png" alt="Google Maps Scraper Actor page on Apify, showing local business search input fields for query, location, and result limits used for competitor discovery" width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data collection runs in three passes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Google Maps search&lt;br&gt;
    -&amp;gt; competitor and location dataset&lt;br&gt;
Google Maps reviews&lt;br&gt;
    -&amp;gt; customer complaint and praise dataset&lt;br&gt;
Google News search&lt;br&gt;
    -&amp;gt; local press and reputation dataset&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For a local SEO agency, this gives one repeatable view of a market. Instead of asking an analyst to search "plumber Dallas", open every competitor, copy ratings, and read reviews manually, the Actor run produces a structured export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 Actors, 3 passes&lt;/strong&gt;: Google Maps Scraper for discovery, Google Maps Reviews Scraper for sentiment, Google News Scraper for reputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maps search caps around 120 results per query&lt;/strong&gt;: keep queries narrow (category + neighborhood) to avoid noisy, unusable output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ratings tell you who's winning; reviews tell you why&lt;/strong&gt;: a 4.8-rating business with 200 reviews and a 4.2-rating business with 4,000 reviews are different competitive stories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow queries beat broad ones&lt;/strong&gt;: "vegan restaurants in Williamsburg" produces a decision-ready dataset; "restaurants in New York" produces noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actor configuration that matters
&lt;/h2&gt;

&lt;p&gt;The Google Maps Scraper supports two modes: &lt;code&gt;search&lt;/code&gt; for a text query and &lt;code&gt;placeUrls&lt;/code&gt; for direct Google Maps place URLs. For broad market discovery, start with &lt;code&gt;searchQuery&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;maxResults&lt;/code&gt;, and &lt;code&gt;language&lt;/code&gt;. The public input schema notes that search mode typically returns up to 120 places for a query, so keep queries narrow enough to avoid noisy results.&lt;/p&gt;

&lt;p&gt;A small first run looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "mode": "search",&lt;br&gt;
  "searchQuery": "orthodontists",&lt;br&gt;
  "location": "Austin, TX",&lt;br&gt;
  "maxResults": 50,&lt;br&gt;
  "language": "en",&lt;br&gt;
  "proxyConfiguration": {&lt;br&gt;
    "useApifyProxy": true&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Google News, the important fields are &lt;code&gt;queries&lt;/code&gt;, &lt;code&gt;maxResultsPerQuery&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;dateRange&lt;/code&gt;, optional &lt;code&gt;siteFilter&lt;/code&gt;, optional &lt;code&gt;excludeWords&lt;/code&gt;, and &lt;code&gt;extractFullText&lt;/code&gt;. Keep &lt;code&gt;extractFullText&lt;/code&gt; off during discovery and enable it only for the articles you actually want to analyze.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;The reason this workflow works is that every stage produces rows you can join later. For the Maps stage, the fields that matter are &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;reviewsCount&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;website&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, and &lt;code&gt;googleMapsUrl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The review and news stages add the context the base listing cannot explain. Join by business name, website domain, and location, then keep the original URLs beside every insight. That way, when a client asks why you flagged a competitor, you can point back to the review or article instead of hand-waving from a summary.&lt;/p&gt;

&lt;p&gt;In a small test run, Google Maps Scraper was asked for five Austin coffee shops. The Actor returned five rows with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;totalScore&lt;/code&gt;, &lt;code&gt;reviewsCount&lt;/code&gt;, &lt;code&gt;categoryName&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;website&lt;/code&gt;, &lt;code&gt;priceLevel&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;placeId&lt;/code&gt;, &lt;code&gt;serviceOptions&lt;/code&gt;, &lt;code&gt;hours&lt;/code&gt;, &lt;code&gt;ratingHistogram&lt;/code&gt;, &lt;code&gt;popularTimesSummary&lt;/code&gt;, &lt;code&gt;photosSample&lt;/code&gt;, &lt;code&gt;googleMapsUrl&lt;/code&gt;, and &lt;code&gt;scrapedAt&lt;/code&gt;. That's enough for the first market snapshot before you even scrape reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Google Maps is the starting point
&lt;/h2&gt;

&lt;p&gt;Google Maps is usually the best first source for local market research because it captures the businesses that customers actually discover. A local competitor might have a weak website and still dominate discovery because its Maps profile has 2,000 reviews and strong proximity coverage.&lt;/p&gt;

&lt;p&gt;The Google Maps Scraper gives you the base table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business name&lt;/li&gt;
&lt;li&gt;category&lt;/li&gt;
&lt;li&gt;address&lt;/li&gt;
&lt;li&gt;phone number&lt;/li&gt;
&lt;li&gt;website&lt;/li&gt;
&lt;li&gt;rating&lt;/li&gt;
&lt;li&gt;review count&lt;/li&gt;
&lt;li&gt;Google Maps URL&lt;/li&gt;
&lt;li&gt;latitude and longitude&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, sort competitors by review volume, distance, category, or rating. For agencies, this is useful before a sales call. For franchise teams, it helps compare location density across neighborhoods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviews turn rankings into explanations
&lt;/h2&gt;

&lt;p&gt;Ratings tell you who is winning. Reviews help explain why.&lt;/p&gt;

&lt;p&gt;A restaurant with a 4.8 rating and 200 reviews is different from a restaurant with a 4.2 rating and 4,000 reviews. The second business may still dominate demand because of location, brand awareness, or delivery coverage. The review text shows the operational pattern behind the score.&lt;/p&gt;

&lt;p&gt;Use the Google Maps Reviews Scraper to pull recent reviews from each competitor, then group comments into recurring themes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow service&lt;/li&gt;
&lt;li&gt;pricing complaints&lt;/li&gt;
&lt;li&gt;staff mentions&lt;/li&gt;
&lt;li&gt;cleanliness&lt;/li&gt;
&lt;li&gt;appointment availability&lt;/li&gt;
&lt;li&gt;product quality&lt;/li&gt;
&lt;li&gt;parking or location issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the dataset becomes useful. A local dentist can see that competitors get repeated complaints about long wait times. A gym can see that customers praise flexible hours. A home services company can spot which competitors are praised for same-day availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  News adds reputation context
&lt;/h2&gt;

&lt;p&gt;The Google News Scraper completes the picture. Local businesses often show up in news for openings, lawsuits, awards, acquisitions, health inspections, funding, or community events.&lt;/p&gt;

&lt;p&gt;The Actor supports keyword searches, date filters, country and language settings, optional full-text extraction, and deduplication across queries. That means you can search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the client brand&lt;/li&gt;
&lt;li&gt;competitor names&lt;/li&gt;
&lt;li&gt;category keywords&lt;/li&gt;
&lt;li&gt;neighborhood + service terms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output gives article title, source, URL, publication date, snippet, and optionally full article text. News volume isn't a perfect popularity metric, but it's a useful reputation signal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdj22wduhpziiedwy614j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdj22wduhpziiedwy614j.png" alt="Google News Scraper Actor page on Apify showing keyword search, date range, and full-text extraction settings used for local news monitoring" width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep the market definition tight.&lt;/strong&gt; "Restaurants in New York" produces noise. "Vegan restaurants in Williamsburg" produces a dataset someone can act on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store historical snapshots.&lt;/strong&gt; A single scrape is useful. Weekly snapshots are better. Review count growth, rating changes, and new competitor appearances matter more over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate business discovery from review scraping.&lt;/strong&gt; Run Maps discovery first, inspect the dataset, then pass selected URLs into the reviews stage. That keeps review scraping focused on relevant competitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use review text carefully.&lt;/strong&gt; Reviews can include personal data. Collect only the fields needed for analysis and avoid unnecessary storage of sensitive reviewer details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broad queries return datasets, not decisions.&lt;/strong&gt; An early test query of "restaurants in Chicago" returned a dataset, but not a decision. Narrowing the search to a category, neighborhood, and use case made the output useful — a smaller, sharper input usually beats a giant scrape.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Custom workflow&lt;/th&gt;
&lt;th&gt;Raw data ownership&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual research&lt;/td&gt;
&lt;td&gt;Analyst time&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Spreadsheet only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local SEO SaaS&lt;/td&gt;
&lt;td&gt;$50-$300+&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrawlerBros on Apify&lt;/td&gt;
&lt;td&gt;Pay per run&lt;/td&gt;
&lt;td&gt;On demand&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the Apify row, check the current Pricing tab on each Actor page before running at scale. These Actors may be priced by compute usage, result/event count, or a platform pricing model that changes over time — the point is you only pay for the runs you need and keep the raw dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can Apify scrape local business data from Google Maps?
&lt;/h2&gt;

&lt;p&gt;Yes. CrawlerBros provides Google Maps Actors that extract structured business and review data from public Google Maps pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can I do with local market intelligence data?
&lt;/h2&gt;

&lt;p&gt;You can monitor competitors, find weak review themes, prioritize sales outreach, compare locations, and feed local SEO dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I need to code this workflow?
&lt;/h2&gt;

&lt;p&gt;No. You can run each Actor from Apify Console and export the datasets. Developers can also automate the workflow with the Apify API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the fastest way to try this?
&lt;/h2&gt;

&lt;p&gt;Start with a small query such as "orthodontists in Austin," run Google Maps Scraper, pick 10 competitors, then run the reviews and news stages.&lt;/p&gt;

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

&lt;p&gt;Start with a small query such as "orthodontists in Austin" or "coffee shops in Denver." Run &lt;a href="https://apify.com/crawlerbros/google-maps-scraper" rel="noopener noreferrer"&gt;Google Maps Scraper&lt;/a&gt;, choose 10 competitors, then run &lt;a href="https://apify.com/crawlerbros/google-maps-reviews-scraper" rel="noopener noreferrer"&gt;Google Maps Reviews Scraper&lt;/a&gt; and &lt;a href="https://apify.com/crawlerbros/google-news-scraper" rel="noopener noreferrer"&gt;Google News Scraper&lt;/a&gt;. You'll know very quickly whether the market is crowded, who owns attention, and what customers are saying.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>programming</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
