<?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: Prime Sieve</title>
    <description>The latest articles on DEV Community by Prime Sieve (@primesieve).</description>
    <link>https://dev.to/primesieve</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081693%2F1d5bd6c2-071c-4b66-9914-b251a6acbcc6.png</url>
      <title>DEV Community: Prime Sieve</title>
      <link>https://dev.to/primesieve</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/primesieve"/>
    <language>en</language>
    <item>
      <title>How I built a Hacker News scraper that pulled 1,000 posts in 10 seconds</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:36:49 +0000</pubDate>
      <link>https://dev.to/primesieve/how-i-built-a-hacker-news-scraper-that-pulled-1000-posts-in-10-seconds-gi3</link>
      <guid>https://dev.to/primesieve/how-i-built-a-hacker-news-scraper-that-pulled-1000-posts-in-10-seconds-gi3</guid>
      <description>&lt;h1&gt;
  
  
  How I built a Hacker News scraper that pulled 1,000 posts in 10 seconds
&lt;/h1&gt;

&lt;p&gt;Most HN scrapers I've seen over-engineer the problem. Proxy rotation, headless browsers, captcha solving — all to pull data that's already exposed through a free JSON API. Here's the version that takes 10 seconds and costs nothing&lt;/p&gt;

&lt;h2&gt;
  
  
  The API nobody charges for
&lt;/h2&gt;

&lt;p&gt;Hacker News uses Algolia to power search, and Algolia exposes a public endpoint with no key required&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://hn.algolia.com/api/v1/search_by_date?tags=story&amp;amp;hitsPerPage=200&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's it. No auth. No rate limit. Returns clean JSON with title, URL, points, comments, author, timestamp — everything you need to analyze HN&lt;/p&gt;

&lt;h2&gt;
  
  
  The fetch (no Apify, no SDK, just urllib
&lt;/h2&gt;

&lt;p&gt;`import json&lt;br&gt;
import urllib.request&lt;br&gt;
import time&lt;/p&gt;

&lt;p&gt;stories = []&lt;br&gt;
for page in range(10):&lt;br&gt;
    url = f"&lt;a href="https://hn.algolia.com/api/v1/search_by_date?tags=story&amp;amp;hitsPerPage=100&amp;amp;page=%7Bpage%7D" rel="noopener noreferrer"&gt;https://hn.algolia.com/api/v1/search_by_date?tags=story&amp;amp;hitsPerPage=100&amp;amp;page={page}&lt;/a&gt;"&lt;br&gt;
    req = urllib.request.Request(url, headers={"User-Agent": "hn-research/1.0"})&lt;br&gt;
    with urllib.request.urlopen(req) as r:&lt;br&gt;
        data = json.loads(r.read())&lt;br&gt;
        stories.extend(data.get("hits", []))&lt;br&gt;
    time.sleep(0.3)  # be polite&lt;/p&gt;

&lt;p&gt;print(f"Pulled {len(stories)} stories")`&lt;/p&gt;

&lt;p&gt;Total runtime on my VPS: about 12 seconds for 1,000 posts. The 0.3s sleep is optional but polite — Algolia serves everyone from the same backend&lt;/p&gt;

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

&lt;p&gt;I ran the script for the last 7 days and bucketed by points&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Median points: 1&lt;/strong&gt;. Half of all HN stories get exactly one upvote&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Only 1.5% reach 50 points&lt;/strong&gt; — that's "visible" by HN's standards&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero posts crossed 500 points&lt;/strong&gt; in the entire 7-day window&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The top story of the week landed at 104 points (a Jane Street reverse engineering writeup). The HN virality curve is steep. If you're posting there hoping for traction, you're competing in a 1.5% funnel&lt;/p&gt;

&lt;h2&gt;
  
  
  When the data was posted (UTC
&lt;/h2&gt;

&lt;p&gt;06:00 UTC    5 posts&lt;br&gt;
07:00 UTC   35 posts&lt;br&gt;
08:00 UTC   28 posts&lt;br&gt;
09:00 UTC   27 posts&lt;br&gt;
10:00 UTC   40 posts&lt;br&gt;
11:00 UTC   46 posts&lt;br&gt;
12:00 UTC   19 posts&lt;br&gt;
(other hours: ~0)&lt;/p&gt;

&lt;p&gt;Almost everything posts between 07:00 and 12:00 UTC (US morning). Counterintuitively, the &lt;em&gt;best&lt;/em&gt; window to post is 06:00–07:00 UTC — you catch the morning traffic spike with less competition&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain analysis
&lt;/h2&gt;

&lt;p&gt;Top linked domains across 1,000 posts&lt;/p&gt;

&lt;p&gt;twitter.com      45&lt;br&gt;
github.com       29&lt;br&gt;
openai.com       18&lt;br&gt;
nytimes.com      13&lt;br&gt;
anthropic.com    13&lt;/p&gt;

&lt;p&gt;AI-related domains are over-represented. HN readers actively look for AI news, so if you're posting in that space, you get structural tailwind&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaged as an Apify actor
&lt;/h2&gt;

&lt;p&gt;I wrapped the script as an Apify actor so I can re-run it weekly without rewriting the boilerplate. The actor charges per result (PAY_PER_EVENT), so cost scales with what you actually pull&lt;/p&gt;

&lt;p&gt;`// src/main.js — Apify actor (free Algolia endpoint)&lt;br&gt;
import { Actor } from 'apify';&lt;/p&gt;

&lt;p&gt;await Actor.main(async () =&amp;gt; {&lt;br&gt;
  const input = await Actor.getInput() || {};&lt;br&gt;
  const queries = input.queries || [];&lt;br&gt;
  const tags = input.tags || 'story';&lt;br&gt;
  const maxResults = Math.min(500, input.maxResults || 100);&lt;/p&gt;

&lt;p&gt;for (const q of queries) {&lt;br&gt;
    const url = &lt;code&gt;https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&amp;amp;tags=${tags}&amp;amp;hitsPerPage=${Math.min(100, maxResults)}&lt;/code&gt;;&lt;br&gt;
    const res = await fetch(url, { headers: { accept: 'application/json' } });&lt;br&gt;
    const data = await res.json();&lt;br&gt;
    for (const hit of data.hits || []) {&lt;br&gt;
      await Actor.pushData({&lt;br&gt;
        title: hit.title || hit.story_title,&lt;br&gt;
        url: hit.url || hit.story_url,&lt;br&gt;
        author: hit.author,&lt;br&gt;
        points: hit.points,&lt;br&gt;
        comments: hit.num_comments,&lt;br&gt;
        createdAt: hit.created_at,&lt;br&gt;
      });&lt;br&gt;
      try { await Actor.charge({ eventName: 'result', count: 1 }); } catch {}&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
});`&lt;/p&gt;

&lt;h2&gt;
  
  
  The full pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pull&lt;/strong&gt; — fetch 1,000 fresh stories (10 seconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Score&lt;/strong&gt; — bucket by points, comments, posting hour (Python pandas, 30 seconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analyze&lt;/strong&gt; — extract the patterns above (5 minutes of thinking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write up&lt;/strong&gt; — turn the data into a Medium article (the long-form version&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total time: about an hour from API call to published article. Total cost: free (Algolia is free, the actor charges per-result on Apify&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;You don't need a scraping service to do competitive research on public forums. HN, Reddit, GitHub, Wikipedia, Stack Overflow — all of them have free public APIs or fetch-clean endpoints. The bottleneck isn't data access, it's analysis&lt;/p&gt;

&lt;p&gt;The same approach works for any forum where posts are timestamped. The trick is: pick a 7-day window, pull everything, score by engagement, and look for the patterns. You don't need ML — a 50-line script does it&lt;/p&gt;

&lt;p&gt;This is part of a broader weekly data routine I run — HN virality, GitHub trends, and (separately) the remote job market. If you like this style of low-cost data analysis, I publish a weekly report on remote hiring signals every Wednesday&lt;/p&gt;

</description>
      <category>python</category>
      <category>data</category>
      <category>scraping</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Remote Engineering Hiring Signal: What 250+ Scraped Jobs Reveal This Week</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:54:51 +0000</pubDate>
      <link>https://dev.to/primesieve/remote-engineering-hiring-signal-what-250-scraped-jobs-reveal-this-week-44hm</link>
      <guid>https://dev.to/primesieve/remote-engineering-hiring-signal-what-250-scraped-jobs-reveal-this-week-44hm</guid>
      <description>&lt;h1&gt;
  
  
  Remote Engineering Hiring Signal: What 250+ Scraped Jobs Reveal This Week
&lt;/h1&gt;

&lt;p&gt;Every week, our automated scrapers index over 250 remote-first engineering job boards, filtering out ghost listings, low-effort recruiter aggregators, and stale postings to find out who is actually hiring senior developers.&lt;/p&gt;

&lt;p&gt;Here is a quick snapshot of this week's data, featured roles, and engineering trends.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 This Week's Market Signal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total Jobs Scanned:&lt;/strong&gt; 256&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-Signal Listings (Grade A/B):&lt;/strong&gt; 217&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top In-Demand Stacks:&lt;/strong&gt; Python, PostgreSQL, React, Node.js, Go, FastAPI, Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Query Breakdown
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile developer remote:&lt;/strong&gt; 55 roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend developer remote:&lt;/strong&gt; 52 roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps engineer remote:&lt;/strong&gt; 52 roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI / ML engineer remote:&lt;/strong&gt; 50 roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full stack engineer remote:&lt;/strong&gt; 47 roles&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏆 Featured Roles (Sample from This Week)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Senior Backend Engineer&lt;/strong&gt; — Nametag (Seattle / Remote)

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Tech:&lt;/em&gt; Python, PostgreSQL, REST&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Signal:&lt;/em&gt; Identity verification platform scaling backend infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Stack Developer&lt;/strong&gt; — Softermii (Los Angeles / Remote)

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Tech:&lt;/em&gt; React, Node.js, PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Signal:&lt;/em&gt; Fintech solutions expanding remote engineering team.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior GoLang Developer&lt;/strong&gt; — ImagineX (Atlanta / Remote)

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Tech:&lt;/em&gt; Go, PostgreSQL, Docker&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Signal:&lt;/em&gt; High-performance backend microservices.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🛠️ Tool of the Week: Donsetch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Donsetch&lt;/strong&gt; — Keyless web fetch/search/crawl for AI agents. Auto bot-wall bypass, clean markdown output. Perfect for building data pipelines without proxy costs. &lt;a href="https://github.com/anthropics/donsetch" rel="noopener noreferrer"&gt;GitHub →&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Get the Full 15-Role Briefing Every Wednesday
&lt;/h3&gt;

&lt;p&gt;This is just a small sample of our weekly briefing. We deliver 5 deep-dive featured roles with direct apply links, plus 10 quick hits and a cold pitch template straight to your inbox every week.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://remotesignal.substack.com" rel="noopener noreferrer"&gt;Subscribe to Remote Signal&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>remote</category>
      <category>hiring</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Wikipedia Scraper That Runs on Apify Without Proxies</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Fri, 28 Aug 2026 10:18:11 +0000</pubDate>
      <link>https://dev.to/primesieve/i-built-a-wikipedia-scraper-that-runs-on-apify-without-proxies-525l</link>
      <guid>https://dev.to/primesieve/i-built-a-wikipedia-scraper-that-runs-on-apify-without-proxies-525l</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Wikipedia Scraper That Runs on Apify Without Proxies
&lt;/h1&gt;

&lt;p&gt;Most Wikipedia scrapers on Apify use Playwright or headless browsers. I built one that hits the MediaWiki API directly — 200 responses, zero proxy costs, 54 lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Wikipedia Data?
&lt;/h2&gt;

&lt;p&gt;Wikipedia is the largest open knowledge base on the internet. 60M+ articles across 300+ languages. Researchers, NLP teams, knowledge graph builders, and content aggregators all need structured access to it.&lt;/p&gt;

&lt;p&gt;The problem: most tools overcomplicate it. They spin up headless browsers to render pages, manage proxy pools to avoid rate limits, and charge you for the overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actor Does
&lt;/h2&gt;

&lt;p&gt;Search any keyword across any Wikipedia language. Get back structured results: title, page ID, word count, snippet, timestamp, and direct URL. Paginate up to 500 results per keyword with automatic offset handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;keywords&lt;/code&gt; — array of search terms&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lang&lt;/code&gt; — language code (default: &lt;code&gt;en&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxResults&lt;/code&gt; — up to 500 per keyword&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output per result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quantum computing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&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;"Quantum computing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pageid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22967&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;85432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wordcount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Quantum computing is a type of computation..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-12-15T10:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://en.wikipedia.org/wiki/Quantum_computing"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The No-Proxy Advantage
&lt;/h2&gt;

&lt;p&gt;Wikipedia's MediaWiki API is open. No authentication required. No rate limit tricks needed if you respect their guidelines.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero proxy costs&lt;/strong&gt; — runs clean on Apify's AWS infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No browser overhead&lt;/strong&gt; — plain HTTP GET, ~250ms per request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — no CAPTCHA challenges, no IP blocks, no flaky browser sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most competing actors charge $3-5 per 1k results and burn proxy credits. This one runs at the Apify free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NLP training data&lt;/strong&gt; — bulk collect article metadata for text classification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge graph construction&lt;/strong&gt; — page IDs + titles + timestamps for entity linking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content gap analysis&lt;/strong&gt; — compare coverage across languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research automation&lt;/strong&gt; — systematic literature discovery on any topic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO research&lt;/strong&gt; — find what Wikipedia covers about your niche&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;

&lt;p&gt;Search for "Wikipedia Search Scraper" on Apify Store, or use the API directly:&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;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/primesievecoder~wikipedia-search-scraper/runs"&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"keywords": ["quantum computing", "machine learning"], "lang": "en", "maxResults": 100}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Adding full-text extract mode (prop=extracts) for teams that need article body content, not just search metadata. Stay tuned.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/primesievecoder" rel="noopener noreferrer"&gt;Prime Sieve&lt;/a&gt; — scraping tools that just work. No proxies, no headless browsers, no drama.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>scraping</category>
      <category>apify</category>
      <category>wikipedia</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Reverse-Engineering YouTube's InnerTube Search API (WEB Client + Continuations)</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Mon, 24 Aug 2026 12:07:08 +0000</pubDate>
      <link>https://dev.to/primesieve/reverse-engineering-youtubes-innertube-search-api-web-client-continuations-480m</link>
      <guid>https://dev.to/primesieve/reverse-engineering-youtubes-innertube-search-api-web-client-continuations-480m</guid>
      <description>&lt;h1&gt;
  
  
  Reverse-Engineering YouTube's InnerTube Search API (WEB Client + Continuations)
&lt;/h1&gt;

&lt;p&gt;YouTube search in a browser loads HTML. YouTube search as a client sends JSON.&lt;/p&gt;

&lt;p&gt;Same query, two paths. The HTML path costs a browser, a consent click, and 2 GB of RAM. The JSON path costs one &lt;code&gt;fetch&lt;/code&gt;. I picked the second.&lt;/p&gt;

&lt;p&gt;This is the shape of YouTube's InnerTube search from the WEB client side — what it sends, what it returns, how it pages — and where I stopped so this stays a tour, not a recipe.&lt;/p&gt;

&lt;p&gt;The actor is live: &lt;strong&gt;primesieve/youtube-search-scraper&lt;/strong&gt; — keywords in, normalized videos out, 20 per page, fetch-clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the browser actually does
&lt;/h2&gt;

&lt;p&gt;Open YouTube, type &lt;code&gt;golang tutorial&lt;/code&gt;, watch network. One POST fires. JSON goes out with &lt;code&gt;query&lt;/code&gt; and a &lt;code&gt;context.client&lt;/code&gt; block. JSON comes back with a tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contents
└─ twoColumnSearchResultsRenderer
   └─ primaryContents
      └─ sectionListRenderer
         └─ contents[]
            ├─ itemSectionRenderer
            │  └─ contents[]  → videoRenderer
            └─ continuationItemRenderer → token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a second bucket for paginated results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;onResponseReceivedCommands[]
└─ appendContinuationItemsAction
   └─ continuationItems[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leaves are &lt;code&gt;videoRenderer&lt;/code&gt; objects — title runs, owner text, view count text, length text, publish text, thumbnail array, videoId. Everything you need is in the leaf. The rest is shelf furniture.&lt;/p&gt;

&lt;p&gt;I will not dump the request body here. The client name, version string, and header shape rotate. Copy-pasting them is how you build a fork that breaks next Tuesday. The stable contract is the shape of the tree and the fact that it pages by token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WEB client
&lt;/h2&gt;

&lt;p&gt;InnerTube has many clients — WEB, MWEB, ANDROID, IOS, TV and more. Each negotiates a slightly different shape and policy. I picked &lt;code&gt;WEB&lt;/code&gt; with &lt;code&gt;DESKTOP&lt;/code&gt; platform because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It answers 200 without a login or consent wall.&lt;/li&gt;
&lt;li&gt;It returns 20 videos per page — the same count the HTML shows.&lt;/li&gt;
&lt;li&gt;It returns &lt;code&gt;visitorData&lt;/code&gt; you can grab from the homepage in one fetch and reuse. Optional, but responses are more stable with it.&lt;/li&gt;
&lt;li&gt;It pages by a single opaque continuation string you send back in the next POST. No cursor math.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Other clients change the shelf mix or require different tokens. WEB is the boring one that answers politely. Boring wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuations, not pages
&lt;/h2&gt;

&lt;p&gt;There is no &lt;code&gt;?page=2&lt;/code&gt;. There is a token at the bottom of page one. Send it back as &lt;code&gt;continuation&lt;/code&gt; in the next body and you get page two. Repeat.&lt;/p&gt;

&lt;p&gt;The walk looks like this at a high level:&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;// pseudocode — shape only, not the real selector or token pick&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxPages&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxResults&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;json&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;postSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&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="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;continuation&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;walkTree&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="c1"&gt;// visit sectionListRenderer → itemSectionRenderer → videoRenderer&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pickContinuation&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="c1"&gt;// prefer the token near the search apiUrl&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;);&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;next&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;videos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;token&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shorts shelves sit in the same parent list as regular shelves. A parser that assumes &lt;code&gt;contents[0]&lt;/code&gt; is always videos misses them. Walk the full list, check each section's type, visit the &lt;code&gt;itemSectionRenderer.contents&lt;/code&gt; bag if present, recurse into &lt;code&gt;sectionListRenderer.contents&lt;/code&gt; when nested. That is the whole traversal.&lt;/p&gt;

&lt;p&gt;Continuation tokens are long opaque strings. Don't decode them. Don't trim them. Forward them verbatim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I left out on purpose
&lt;/h2&gt;

&lt;p&gt;No &lt;code&gt;INNERTUBE_API_KEY&lt;/code&gt;, no client version string, no visitorData regex, no header map, no exact JSON path for the token. Those change. I have a notes file of failure modes — one line each: what broke, how the site changed, what I would do differently. Most are boring. A few become README warnings. Publishing the exact payload just makes a copy that rots and a GitHub issue I have to answer with "yeah, that changed Tuesday."&lt;/p&gt;

&lt;p&gt;What matters to you is the interface — yours, not mine:&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;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lofi hip hop"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;Out:&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;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lofi hip hop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5qap5aO4i9A"&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;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"views"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thumbnail"&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://i.ytimg.com/vi/5qap5aO4i9A/hqdefault.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.youtube.com/watch?v=5qap5aO4i9A"&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;Same five fields in, same eight fields out, keyword after keyword. My parse ladder is my maintenance burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetch-clean is the check that mattered
&lt;/h2&gt;

&lt;p&gt;Before writing anything I ran one fetch with a desktop user-agent against the homepage. 200. Then one POST against the search endpoint. 200. No proxy. That two-line check killed the browser branch before it started.&lt;/p&gt;

&lt;p&gt;The bar for fetch-clean is low and the savings are high: no Playwright, no Puppeteer, no 2 GB browser, no consent dialog to click, no scroll loop to babysit. Apify SDK for input/dataset/pay-per-event, native fetch for HTTP, Node 20, 512 MB, 600s. One file in &lt;code&gt;src/main.js&lt;/code&gt;. The boring stack that never wakes you at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three small lessons from shipping it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. visitorData is optional but cheap.&lt;/strong&gt; Fetch the homepage once, grab the token if the HTML has one, fall back to a default if not. If the homepage fetch flakes, don't fail the run. Small stability win, not a dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never assume flat contents.&lt;/strong&gt; The tree mixes video sections and shorts sections and continuation sentinels in one array. Recurse. Check the key that is present — &lt;code&gt;itemSectionRenderer&lt;/code&gt;, &lt;code&gt;continuationItemRenderer&lt;/code&gt;, &lt;code&gt;sectionListRenderer&lt;/code&gt; — and handle each. Flat-map misses shelves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Empty + no next means stop.&lt;/strong&gt; Zero videos and no continuation is end-of-results, not an error. Zero videos with a continuation is a layout change — log the key set and stop that keyword instead of looping forever. A clear stop beats a silent spin.&lt;/p&gt;

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

&lt;p&gt;The pricing is boring on purpose: &lt;strong&gt;$0.80 per 1,000 videos&lt;/strong&gt;, pay-per-event, no tiers.&lt;/p&gt;

&lt;p&gt;Browser-based YouTube actors cluster around $0.50–$5 per 1k because they pay the browser tax. This one doesn't. 1k = $0.80, 10k = $8. Quote it without a table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/primesieve/youtube-search-scraper" rel="noopener noreferrer"&gt;https://apify.com/primesieve/youtube-search-scraper&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start with one keyword and &lt;code&gt;maxResults: 50&lt;/code&gt;. Push to 500 when you trust the shape. Same schema. Same price. Boring on purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Prime Sieve — I build small tools that do one thing honestly. I write about what broke, not just what shipped. More at &lt;a href="https://github.com/primesievecoder" rel="noopener noreferrer"&gt;apify.com/Prime-Sieve&lt;/a&gt; and &lt;a href="https://github.com/primesievecoder" rel="noopener noreferrer"&gt;github.com/primesievecoder&lt;/a&gt;. Thanks for trying it. If it breaks, tell me. It will break.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>youtube</category>
      <category>javascript</category>
      <category>apify</category>
    </item>
    <item>
      <title>I built an OLX scraper for 24 countries — the boring version that actually ships</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Sat, 22 Aug 2026 09:49:04 +0000</pubDate>
      <link>https://dev.to/primesieve/i-built-an-olx-scraper-for-24-countries-the-boring-version-that-actually-ships-432p</link>
      <guid>https://dev.to/primesieve/i-built-an-olx-scraper-for-24-countries-the-boring-version-that-actually-ships-432p</guid>
      <description>&lt;h1&gt;
  
  
  I built an OLX scraper for 24 countries — the boring version that actually ships
&lt;/h1&gt;

&lt;p&gt;OLX runs classifieds in about two dozen countries. Same brand, different domains, different anti-bot setups. Everyone scraping it does one country at a time.&lt;/p&gt;

&lt;p&gt;I got tired of forking.&lt;/p&gt;

&lt;p&gt;So I put 24 countries behind one input. &lt;code&gt;country: "id"&lt;/code&gt; or &lt;code&gt;country: "pl"&lt;/code&gt; or &lt;code&gt;country: "br"&lt;/code&gt; — same schema out. It's live on Apify as &lt;code&gt;primesieve/olx-global-scraper&lt;/code&gt;. One file. No browser. Here is the boring part that matters.&lt;/p&gt;

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

&lt;p&gt;Input:&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;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"iphone 13"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proxyConfiguration"&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;"useApifyProxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"apifyProxyGroups"&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;"RESIDENTIAL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;country&lt;/code&gt; — two-letter code (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;pl&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;br&lt;/code&gt;, &lt;code&gt;ua&lt;/code&gt;, &lt;code&gt;pt&lt;/code&gt;, &lt;code&gt;ro&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;kz&lt;/code&gt;, &lt;code&gt;uz&lt;/code&gt;, &lt;code&gt;pk&lt;/code&gt;, &lt;code&gt;za&lt;/code&gt;, &lt;code&gt;ng&lt;/code&gt;, &lt;code&gt;ke&lt;/code&gt;, &lt;code&gt;eg&lt;/code&gt;, &lt;code&gt;lb&lt;/code&gt;, &lt;code&gt;ph&lt;/code&gt;, &lt;code&gt;co&lt;/code&gt;, &lt;code&gt;ar&lt;/code&gt;, &lt;code&gt;pe&lt;/code&gt;, &lt;code&gt;ec&lt;/code&gt;, &lt;code&gt;gt&lt;/code&gt;, &lt;code&gt;az&lt;/code&gt;, &lt;code&gt;ma&lt;/code&gt;). Default &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keywords&lt;/code&gt; — one or more search terms. Each runs sequentially.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxResults&lt;/code&gt; / &lt;code&gt;maxPages&lt;/code&gt; — caps. Defaults 50 / 3, max 1000 / 30.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proxyConfiguration&lt;/code&gt; — optional for Indonesia, required for the other 23.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output — same shape every country:&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;"listingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789"&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;"iPhone 13 128GB mulus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6500000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rp 6.500.000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IDR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jakarta Selatan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tebet, Jakarta Selatan, DKI Jakarta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"images"&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://...jpg"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thumbnailUrl"&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://...jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listingUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.olx.co.id/item/123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&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;Title, price (numeric plus display text), currency, location, images, URL. No seller PII beyond what the listing page shows. No tricks.&lt;/p&gt;

&lt;p&gt;Try: &lt;a href="https://apify.com/primesieve/olx-global-scraper" rel="noopener noreferrer"&gt;https://apify.com/primesieve/olx-global-scraper&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// no playwright, no puppeteer&lt;/span&gt;
&lt;span class="c1"&gt;// apify + fetch + cheerio. That's it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scraper is one file. Apify SDK for input, dataset, and pay-per-event. Native &lt;code&gt;fetch&lt;/code&gt; for HTTP. &lt;code&gt;cheerio&lt;/code&gt; for the HTML path. Undici &lt;code&gt;ProxyAgent&lt;/code&gt; when a proxy is configured. Node 20, 512 MB, 600s timeout.&lt;/p&gt;

&lt;p&gt;I check the endpoint before I write the scraper. Indonesia answered with clean JSON and no proxy. That is the exception. Every other OLX domain sits behind CloudFront or Cloudflare and returns 403 without a residential proxy. Knowing that before you code saves a whole debugging session.&lt;/p&gt;

&lt;p&gt;Browsers are expensive. Boring code is cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two paths, one schema
&lt;/h2&gt;

&lt;p&gt;I will not paste internal URLs or selectors. Here is the shape instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indonesia&lt;/strong&gt; — fetch-clean path. No proxy needed. The platform returns structured JSON. I map it to the normalized schema and push rows. If you run only &lt;code&gt;country: "id"&lt;/code&gt;, you do not need to configure a proxy at all. Existing users of my single-country OLX actor keep the same default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every other country&lt;/strong&gt; — proxied HTML path. Requires Apify residential proxy. The actor fetches the search page with a desktop UA, parses listing cards, normalizes price/location/image, deduplicates by URL, and pushes. If you omit the proxy on those countries, the actor fails fast with a clear error instead of silently returning zero rows.&lt;/p&gt;

&lt;p&gt;Why two paths? Because the web is not uniform. Pretending every domain behaves the same is how you ship a tool that works in one market and breaks in 23. Two paths is honest. One schema is usable.&lt;/p&gt;

&lt;p&gt;What I deliberately leave out of the README: exact endpoints, exact selectors, exact paging params. Those change. The contract that matters to you is input and output, not my parse ladder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing that does not need a spreadsheet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$0.003 per product&lt;/strong&gt; — &lt;code&gt;PAY_PER_EVENT&lt;/code&gt;. One event is one pushed listing. No tiers, no credits, no contact-sales.&lt;/p&gt;

&lt;p&gt;The math is boring on purpose. 1,000 listings = $3. 10,000 = $30. You can quote it to your boss without a tier table.&lt;/p&gt;

&lt;p&gt;For comparison, most marketplace scrapers on the store charge tiered per-1k with opaque volume breaks. That works for enterprise contracts. It is bad for a solo dev pulling 2,000 rows for a price tracker. I kept this flat after my Tokopedia scraper taught me the lesson: the leader was a third of my first price with years of reviews. I checked the store before I checked my code this time.&lt;/p&gt;

&lt;p&gt;Indonesia default stays the same so current users see no billing change. Other countries just add the proxy cost from Apify (residential usage). The product charge itself stays $0.003 everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned shipping it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Validate country early.&lt;/strong&gt; The actor throws on unknown codes with the allowed list in the message. A typo in &lt;code&gt;country&lt;/code&gt; should fail in second one, not after three pages of empty fetches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fail loud on zero cards.&lt;/strong&gt; If a proxied fetch returns HTML with no listing cards, that is not "zero results" — it is a WAF or a layout change. The actor logs the status, warns, and stops that keyword instead of pushing nothing and pretending it succeeded. Silent zero-row runs are how you corrupt a dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deduplicate by URL.&lt;/strong&gt; Paging overlaps. Sponsored placements repeat. A &lt;code&gt;Set&lt;/code&gt; on &lt;code&gt;listingUrl&lt;/code&gt; costs nothing and saves you cleaning it later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Keep the input schema small.&lt;/strong&gt; Five fields. Two enums. No 15-param form. The Apify input schema validates &lt;code&gt;editor&lt;/code&gt; types (&lt;code&gt;stringList&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;) — every property needs one or the build fails. I learned that on the Tokopedia actor the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Sleep between pages.&lt;/strong&gt; 500ms on the JSON path, 900ms on HTML. Not because the code is slow. Because being polite is cheaper than being blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;This is on a free-tier Apify account. Zero users today. It will not make me rich overnight. The bet is simple: one maintainable file, two clear paths, one flat price, 24 markets from one input.&lt;/p&gt;

&lt;p&gt;Boring code never wakes you at 2am. That is the whole pitch.&lt;/p&gt;

&lt;p&gt;The actor: &lt;strong&gt;&lt;a href="https://apify.com/primesieve/olx-global-scraper" rel="noopener noreferrer"&gt;https://apify.com/primesieve/olx-global-scraper&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it breaks, tell me. It will break — sites change on Tuesdays. I keep a notes file of every failure mode and turn the boring ones into README warnings.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Prime Sieve — I build small tools that do one thing honestly. More at &lt;a href="https://apify.com/Prime-Sieve" rel="noopener noreferrer"&gt;apify.com/Prime-Sieve&lt;/a&gt; and &lt;a href="https://github.com/primesievecoder" rel="noopener noreferrer"&gt;github.com/primesievecoder&lt;/a&gt;. Thanks for trying it. If it breaks, tell me. It will break.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I wrote a scraper for a government agency's announcements — and learned why boring tools win</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Tue, 18 Aug 2026 06:02:08 +0000</pubDate>
      <link>https://dev.to/primesieve/i-wrote-a-scraper-for-a-government-agencys-announcements-and-learned-why-boring-tools-win-2blk</link>
      <guid>https://dev.to/primesieve/i-wrote-a-scraper-for-a-government-agencys-announcements-and-learned-why-boring-tools-win-2blk</guid>
      <description>&lt;h1&gt;
  
  
  I wrote a scraper for a government agency's announcements — and learned why boring tools win
&lt;/h1&gt;

&lt;p&gt;Every morning at 6 AM, a small Python script on one VPS checks the Indonesian food agency's announcement page. If there's something new, it saves the record to a JSON file. That's the whole job.&lt;/p&gt;

&lt;p&gt;I built it because I wanted to track food-policy announcements without refreshing a website by hand. It runs on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One VPS&lt;/strong&gt; — no cluster, no Kubernetes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cron&lt;/strong&gt; — five lines in a crontab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;plain JSON files&lt;/strong&gt; — no database, no ORM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;static hosting&lt;/strong&gt; — the output is a public page anyone can browse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire stack. Here's why it's deliberately boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The machine
&lt;/h2&gt;

&lt;p&gt;A single Linux box. My scraper pulls a few hundred records a day — that does not need a fleet. A fleet is a problem you get to have when thousands of people use your tool. When that happens, I'll rent a second box and update a config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduler
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 6 * * * cd /home/ubuntu/scraper &amp;amp;&amp;amp; ./run.sh &amp;gt;&amp;gt; logs/cron.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cron gets mocked for being ancient. It deserves respect: it has never crashed, never needed a migration, and every sysadmin alive can read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output
&lt;/h2&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&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;"Bapanas: rice stock stable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-16"&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;Plain files. When a tool's whole job is &lt;em&gt;fetch and reshape&lt;/em&gt;, the database is a file. Static hosting is free, fast, and impossible to take down by accident.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Boring code is never debugged at 2am.&lt;/strong&gt; Fancy stacks break in fancy ways. Files and cron break in ways you can see in one &lt;code&gt;cat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs are your friend.&lt;/strong&gt; Every run writes to &lt;code&gt;logs/cron.log&lt;/code&gt;. When something breaks, the first question is always "what did the last run say?" — and the answer is in a text file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the smallest thing that works.&lt;/strong&gt; I was tempted to add a queue, a worker pool, a dashboard. None of it was needed. The scraper ran for 40 days before I touched it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public data deserves public tools.&lt;/strong&gt; This one reads government announcements — no ToS risk, no auth, no ethical gray zone. It's open source because there was no reason not to be.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;The first version broke on the third day. The agency changed their HTML slightly. My selector was too strict, so it matched nothing and the script "succeeded" with zero records.&lt;/p&gt;

&lt;p&gt;Fix: validate output. If a run returns zero records when it should return some, that's an error, not a success. Now the script fails loudly instead of failing quietly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: 0 records scraped, expected &amp;gt; 0 — aborting, not overwriting data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line has saved me more times than any framework ever has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;If you're building a product with users, concurrent jobs, and real state — go rent the fleet, you'll need it. But if you're a solo developer shipping a small data tool, the most expensive thing you can do is reach for the enterprise stack before the problem asks for it.&lt;/p&gt;

&lt;p&gt;The repo is public: &lt;a href="https://github.com/primesievecoder/bapanas-news-tracker" rel="noopener noreferrer"&gt;bapanas-news-tracker&lt;/a&gt; — 2,000+ posts, one file, no API key, no browser.&lt;/p&gt;

&lt;p&gt;I'm Prime Sieve — I build small data tools and write about them. More at &lt;a href="https://apify.com/Prime-Sieve" rel="noopener noreferrer"&gt;apify.com/Prime-Sieve&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>beginners</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I shipped a Tokopedia scraper that undercuts the incumbents 5x — here's the boring part</title>
      <dc:creator>Prime Sieve</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:45:31 +0000</pubDate>
      <link>https://dev.to/primesieve/i-shipped-a-tokopedia-scraper-that-undercuts-the-incumbents-5x-heres-the-boring-part-33lf</link>
      <guid>https://dev.to/primesieve/i-shipped-a-tokopedia-scraper-that-undercuts-the-incumbents-5x-heres-the-boring-part-33lf</guid>
      <description>&lt;h1&gt;
  
  
  I shipped a Tokopedia scraper that undercuts the incumbents 5x — here's the boring part
&lt;/h1&gt;

&lt;p&gt;Indonesia's biggest marketplace has a data problem: everyone wants to know what sells, at what price, from which shops — but the official route is a walled garden. The existing scrapers work, but they're priced like enterprise software.&lt;/p&gt;

&lt;p&gt;So I built the boring version. One file. Plain fetch. No browser. Flat &lt;strong&gt;$0.005 per result&lt;/strong&gt; — about 5x cheaper than the incumbents' per-1k tiered pricing.&lt;/p&gt;

&lt;p&gt;It's live now on Apify: &lt;a href="https://apify.com/primesieve/tokopedia-search-scraper" rel="noopener noreferrer"&gt;primesieve/tokopedia-search-scraper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's what actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// no playwright, no puppeteer, no browser at all&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://gql.tokopedia.com/graphql&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="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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokopedia's public GraphQL endpoint (&lt;code&gt;gql.tokopedia.com/graphql&lt;/code&gt;) serves search results to a plain POST with the same params their own website uses. No API key, no login, no headless browser burning 2GB of RAM per run.&lt;/p&gt;

&lt;p&gt;That's the whole trick: &lt;strong&gt;find the endpoint the website already uses, then call it politely.&lt;/strong&gt; A scraper that fetches 50 results should not spin up a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why flat pricing wins
&lt;/h2&gt;

&lt;p&gt;The incumbents charge per-1k-result tiers that get cheaper at volume but are opaque to quote. I charge a flat $0.005 per result, always. No tier tables, no "contact sales", no surprises at invoice time.&lt;/p&gt;

&lt;p&gt;For a user pulling 10,000 results a month, that's the difference between a spreadsheet of tiered line items and one predictable number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned shipping it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Verify the data source before writing code.&lt;/strong&gt; My first target was Shopee — bigger market, more users. Shopee's API hard-blocks datacenter IPs (error 90309999). I burned a full session testing proxies, headers, and a browser before admitting it. Tokopedia's GraphQL answered on the first try. The lesson: check the source first, code second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The schema wants an &lt;code&gt;editor&lt;/code&gt; field.&lt;/strong&gt; Apify's input schema validation rejected my first push — every property needs an &lt;code&gt;editor&lt;/code&gt; type (&lt;code&gt;stringList&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;). One line each, but it cost a failed build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Test locally, then on the platform.&lt;/strong&gt; Local runs with &lt;code&gt;APIFY_LOCAL_STORAGE_DIR&lt;/code&gt; caught my doubled-URL bug before it hit production. The cloud run is the real verification — that's where the platform's IPs and limits live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The output schema is a publication requirement.&lt;/strong&gt; Apify won't let you publish an actor whose default build has no output schema. It's a small JSON file — add it before you try to go public, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;This won't make me rich overnight. It's one actor on a free-tier account with 0 users so far. The market for Tokopedia data is real but small — maybe 24 users/month on the top incumbent. The bet is simple: &lt;strong&gt;flat pricing + a boring, working tool beats tiered pricing + enterprise theater&lt;/strong&gt; for the people who actually need this data.&lt;/p&gt;

&lt;p&gt;The code is deliberately unremarkable. That's the point. Boring code never breaks at 2 AM.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Prime Sieve. I build boring tools that work — one scraper at a time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>ecommerce</category>
      <category>opensource</category>
      <category>apify</category>
    </item>
  </channel>
</rss>
