<?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: Ayan Pahwa</title>
    <description>The latest articles on DEV Community by Ayan Pahwa (@iayanpahwa).</description>
    <link>https://dev.to/iayanpahwa</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F873075%2F4be7029b-bbfa-4a1e-8090-9e7e561d96a0.jpeg</url>
      <title>DEV Community: Ayan Pahwa</title>
      <link>https://dev.to/iayanpahwa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iayanpahwa"/>
    <language>en</language>
    <item>
      <title>My agentic coding setup: Claude Code, multi-agent orchestration, and more</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:34:45 +0000</pubDate>
      <link>https://dev.to/extractdata/my-agentic-coding-setup-claude-code-multi-agent-orchestration-and-more-4naa</link>
      <guid>https://dev.to/extractdata/my-agentic-coding-setup-claude-code-multi-agent-orchestration-and-more-4naa</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F5dr15k8q99j5bme2k269.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.amazonaws.com%2Fuploads%2Farticles%2F5dr15k8q99j5bme2k269.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every new agentic coding tool arrives with a version of the same implicit promise: this one will change how you build. I spent a good part of last year installing tools on that promise, configuring them, hitting their limits, and then either reaching for the next release or quietly uninstalling and going back to basics. The result, for a while, was a collection of half-configured assistants that each needed babysitting before they could help with anything.&lt;br&gt;
What I have now is the setup that survived that process, not because the tools are exceptional in isolation, but because I made deliberate choices about what each one is actually for, what it is not for, and how they hand off to each other. I work on main projects at Zyte, on side projects, and on web scraping work, and this setup handles all three without requiring reconfiguration between them. This is that setup: the tools I kept, the habits that hold it together, and the reasoning behind each decision.&lt;/p&gt;
&lt;h2&gt;
  
  
  My unfair advantage
&lt;/h2&gt;

&lt;p&gt;Before diving into the setup itself, I want to say something about what actually makes an agentic setup work, because it is not the tools.&lt;br&gt;
I have been writing code for more than a decade, starting with Embedded C and C++ before gradually moving to higher-level languages and more recently into Python and web scraping. That background means I can usually tell when an agent is on the right track, when it is confidently producing something plausible but wrong, and when it is about to do something I will spend the next hour undoing. I do not need to read every line it writes to know whether the approach is sound. That accumulated judgment is the unfair advantage: years of building a mental model of how code actually behaves, which now applies directly to supervising what an agent produces.&lt;br&gt;
But this is my advantage, not a universal prescription. Yours is different, and your setup should reflect that. If you have spent years in SEO, your unfair advantage is knowing precisely what good output looks like, what a manipulable signal looks like, and what an agent is getting subtly wrong before the metrics catch it. There are already excellent SEO-specific Claude skills available, and building a team of sub-agents around them (one for technical audits, one for content, one for structured data) with your domain knowledge as the quality filter is a genuinely powerful setup. If your background is in data engineering, you know what a clean pipeline looks like and what a silently broken one looks like, which is exactly the kind of judgment an agent cannot supply for itself. If you come from finance, security, or product management, the same principle holds.&lt;br&gt;
The point is not that deep coding experience is required. Agentic tools amplify whatever domain judgment you already have. Think about where that knowledge lives in your case, and build your setup around it rather than copying someone else's wholesale. Everything in this post is what works for me and my context. Take what fits and ignore the rest.&lt;/p&gt;
&lt;h2&gt;
  
  
  The workspace that opens itself
&lt;/h2&gt;

&lt;p&gt;The first friction point I fixed was the startup ritual. Every morning I was opening VS Code, arranging panels, launching a terminal, opening Claude Code, and getting everything positioned before I could do anything useful. Five minutes of overhead that was really ten minutes once you account for the mental cost of doing it on autopilot.&lt;br&gt;
I now have a &lt;code&gt;cw&lt;/code&gt; function in my &lt;code&gt;~/.zshrc&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;cw&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  code &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'
    tell application "Visual Studio Code" to activate
    delay 0.5
    tell application "System Events"
      key code 53 using {command down, shift down}
      delay 0.3
      key code 50 using {control down}
    end tell
  '&lt;/span&gt; &amp;amp;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;cw&lt;/code&gt; in any directory and VS Code opens in that directory, then AppleScript fires after three seconds to focus the window and drop you straight into the terminal panel where &lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; is waiting. One command, full workspace ready.&lt;br&gt;
The layout is fixed: file explorer on the left, terminal strip at the bottom, the agent chat panel in the middle, and outputs on the right. The chat pane is centered, not tucked in the sidebar, and that placement is deliberate. When the agent is your primary collaborator, putting it in the sidebar demotes it spatially. The center position is a constant reminder that orchestration is the primary activity and everything else supports it. For inline code review without breaking context, the Codex plugin for Claude runs directly in the editor.&lt;/p&gt;


&lt;h2&gt;
  
  
  Plan first, always
&lt;/h2&gt;

&lt;p&gt;The habit that improved my output more than any tool was deciding that every task, regardless of scale, starts in plan mode before any file is touched. No exceptions.&lt;br&gt;
Plan mode forces the agent to surface its assumptions, propose a concrete approach, and wait for sign-off before it touches anything. The default behavior of most agentic tools is to start executing immediately, and high-confidence execution in the wrong direction is the failure mode I have run into most. The agent is rarely wrong because the code is bad; it is wrong because it interpreted the brief differently than I intended, and three minutes of planning would have caught the gap.&lt;br&gt;
I did not arrive at this habit entirely on my own. Dario Amodei, CEO of Anthropic, mentioned in a podcast that he spends the majority of his time in plan mode when working with Claude, and that once the plan is solid the actual execution becomes relatively straightforward. That framing stuck with me. If the person building the model treats planning as the primary activity, it is probably worth taking seriously.&lt;br&gt;
For tasks where the problem is still fuzzy in my own head, I dictate into plan mode using &lt;a href="https://wisprflow.ai" rel="noopener noreferrer"&gt;WisprFlow&lt;/a&gt;. This is not just a comfort choice. I have noticed consistently that my spoken prompts produce better results than my typed ones: speaking forces me to construct a full sentence rather than tapping out a telegraphic shorthand, and that extra formality in the brief translates directly into more precise agent output. Describing the problem, the likely approach, and any constraints out loud usually clarifies the brief before the agent has responded, which means the plan mode exchange is a confirmation rather than a negotiation.&lt;br&gt;
Something I have been testing recently on the planning side: Claude Code's &lt;code&gt;/goal&lt;/code&gt; command. The idea is straightforward: before anything else in a session, you set a high-level goal that the agent holds as a persistent north star throughout all its subsequent actions. Where plan mode answers "how do we approach this specific task", &lt;code&gt;/goal&lt;/code&gt; answers "what is this entire session ultimately in service of." I came across the same concept in Codex and liked the forcing function it created: it keeps a long session from gradually drifting away from what you actually opened it to achieve. I am still finding the edges of how to use it well, but the principle is sound: the more clearly you can state what done looks like before the first message, the less corrective steering you need to do mid-session. If you try it, be specific: "refactor the auth module to remove the session token storage" will serve you better than "clean up the auth code."&lt;/p&gt;
&lt;h2&gt;
  
  
  Two tools, different jobs
&lt;/h2&gt;

&lt;p&gt;One thing worth saying before getting into the specifics: the agentic coding space is moving faster than almost any other area of software right now. Every major provider is shipping changes to tooling, pricing, context limits, and model capabilities on a cadence that would have seemed unrealistic two years ago. That pace is exciting, but it also means that going completely all-in on a single vendor is a real risk. If one provider changes pricing, deprecates a model, or ships a breaking change to their CLI, a setup that depends entirely on them stops working. The practical response is to not let that happen: maintain flexibility, keep alternatives warm, and make sure switching costs stay low.&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.amazonaws.com%2Fuploads%2Farticles%2Fpisr7sq6cm9znd77wvij.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.amazonaws.com%2Fuploads%2Farticles%2Fpisr7sq6cm9znd77wvij.png" alt=" " width="799" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My primary tool is &lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; CLI with official Anthropic models, and it is where the majority of my serious work happens. I run it on the Claude API pay-per-usage plan, which means I pay for what I use and nothing beyond that. No monthly seat fee accumulating on days when I am not writing code, and no "I am already paying for it" pressure to keep the agent running past the point of diminishing returns. I also keep &lt;a href="https://chatgpt.com/codex" rel="noopener noreferrer"&gt;Codex&lt;/a&gt; in the mix via the Mac desktop app, not as a replacement, but as a parallel tool I use enough to stay current with how it is developing.&lt;br&gt;
For model experimentation and usage overflow, I use &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt; with &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; via bring-your-own-key (BYOK). This is the experimentation layer and, frankly, the hedge against vendor lock-in. When a new model appears and I want to try it against a real task before committing to it in my main workflow, I reach for OpenCode. My &lt;code&gt;~/.aliases&lt;/code&gt; file has eight model shortcuts that make switching a single word in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;oc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --model openrouter/anthropic/claude-sonnet-4-6'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-ds&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --model openrouter/deepseek/deepseek-chat-v3-1'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-free&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --model openrouter/deepseek/deepseek-r1:free'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-qwen&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --model openrouter/qwen/qwen3-coder'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-gemini&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --model openrouter/google/gemini-2.5-pro'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-opus&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --model openrouter/anthropic/claude-opus-4-7'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-cost&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --usage'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;oc-stats&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'opencode --stats'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;oc-cost&lt;/code&gt; and &lt;code&gt;oc-stats&lt;/code&gt; are not cosmetic shortcuts. The pay-per-usage model only works as a cost discipline if you can see what you are spending.&lt;br&gt;
OpenRouter also has an auto-router option (&lt;code&gt;openrouter/auto&lt;/code&gt;) that selects the best available model for each prompt automatically, which is genuinely useful when you are unsure which model fits a task and do not want to think about it. My &lt;code&gt;opencode.json&lt;/code&gt; config defines three routing entries that cover the main scenarios:&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;"openrouter/auto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Auto Router (picks best model for prompt)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_call"&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;"limit"&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;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8192&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="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"openrouter/free"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Free Router (picks best free model)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_call"&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;"limit"&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;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4096&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="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"openrouter/pareto-code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pareto Code Router (auto-routes coding tasks)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_call"&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;"limit"&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;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;auto&lt;/code&gt; is the general-purpose router. &lt;code&gt;free&lt;/code&gt; picks the best available free model, which is what I reach for when testing something throwaway. &lt;code&gt;pareto-code&lt;/code&gt; is a coding-specific router that OpenRouter maintains separately, optimized for code tasks rather than general prompts.&lt;br&gt;
Beyond Claude overflow, I use OpenRouter's free tier for testing with tools like OpenClaw and the Hermes agent, where spending money on a model I am just kicking the tires on makes no sense. This side of the setup is almost entirely for side projects and learning how different models actually behave on real tasks rather than benchmarks. My personal ranking after a fair amount of experimentation, in order: Qwen3 Coder, DeepSeek, MiniMax, Kimi, and Gemma. Qwen and DeepSeek are consistently good on code; MiniMax and Kimi are worth watching for longer-context tasks; Gemma punches above its weight for its size.&lt;br&gt;
&lt;strong&gt;A note for developers still on Claude Pro ($20/month):&lt;/strong&gt; the plan uses rolling usage windows that reset approximately every five hours. If you plan to code from noon, send Claude a short message at 7 AM. Your window resets right as you sit down, and the next reset lands around 5 PM, which means you can run two back-to-back full sessions from noon through the evening without hitting a cap mid-task. The exact window length has shifted with recent Anthropic updates, so check your own account to calibrate, but the principle holds: prime your session before you need it, not after you have already hit the limit. Since switching to pay-per-usage I no longer need this trick, but it was genuinely useful for the two years I ran on the flat-rate plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four-agent team
&lt;/h2&gt;

&lt;p&gt;Running a general-purpose agent at every task is a bit like asking one person to handle architecture, implementation, code review, and codebase archaeology simultaneously, and expecting them to be equally good at all four. I took inspiration from Gary Tan, CEO of Y Combinator, who described his own layered agent stack (which he calls GStack) as a way of giving each model a specific role rather than asking one model to do everything. I am not using GStack directly, but the framing shaped how I think about agent design: specialize the agents, not the prompts.&lt;/p&gt;



&lt;p&gt;In &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt;, I have defined four named agents, each with a specific model, a specific role, and a specific permission scope.&lt;br&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/architect"&gt;@architect&lt;/a&gt;&lt;/strong&gt; runs on Claude Sonnet 4.6 and is read-only: no edit or bash access. It asks clarifying questions first, then produces ASCII diagrams and a numbered implementation plan. When it needs to understand the codebase before it can plan, it invokes &lt;a class="mentioned-user" href="https://dev.to/scout"&gt;@scout&lt;/a&gt;. The output is written to be clear enough for a junior developer, or for a cheaper model like DeepSeek, to execute without interpretation.&lt;br&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/scout"&gt;@scout&lt;/a&gt;&lt;/strong&gt; runs on Gemini 2.5 Flash with a one-million token context window and is also read-only. It traces call chains, maps data flow, and produces structured reports with full file paths and line numbers. The large context window makes it the right choice for reading substantial portions of an unfamiliar codebase without losing thread.&lt;br&gt;
&lt;strong&gt;@coder&lt;/strong&gt; runs on DeepSeek V3.1, configured at 40 steps and temperature 0.1. It follows the architect's plan exactly and does not extend scope. Before marking any task complete, it invokes &lt;a class="mentioned-user" href="https://dev.to/reviewer"&gt;@reviewer&lt;/a&gt;.&lt;br&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/reviewer"&gt;@reviewer&lt;/a&gt;&lt;/strong&gt; runs on Qwen3 Coder, is read-only, and works through a fixed priority order: security vulnerabilities first, then logic errors, missing error handling, performance bottlenecks, and finally code clarity. It cites exact file paths and line numbers for everything it flags.&lt;br&gt;
The permission constraints are what most people skip, and they are what matter most. A read-only agent cannot accidentally delete files or run shell commands, which limits the blast radius when an agent misreads an instruction. In Claude Code, I apply the same model-tiering logic via the native multi-agent orchestrator: Claude Opus 4.7 for planning, Claude Sonnet 4.6 for execution, and Claude Haiku 4.5 for admin tasks like git summaries and log triage. For the heaviest parallel projects, I use &lt;a href="https://conductor.build" rel="noopener noreferrer"&gt;Conductor&lt;/a&gt;, which runs multiple Claude Code and Codex instances across separate areas of a codebase without context bleed between them.&lt;br&gt;
A word on scale: I am not trying to run 20 agents across 10 projects simultaneously, and I am not optimizing for that. At any given time I work across two or three projects at most, because beyond that I notice the creative block creeping in and the context-switching cost becoming real. Four concurrent agents is my personal ceiling before things start feeling chaotic rather than productive. Running more agents than you can coherently supervise is not a productivity gain; it is just noise with extra steps. The right number is the one where you still know what each agent is doing and why.&lt;/p&gt;
&lt;h2&gt;
  
  
  Teaching agents to remember: the CLAUDE.md file
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5u8vpgp3besqaol48gyw.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.amazonaws.com%2Fuploads%2Farticles%2F5u8vpgp3besqaol48gyw.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the thing nobody tells you when you start using agentic tools seriously: the agent forgets everything between sessions. Every conversation starts from zero. Your project conventions, the architectural decisions you made three weeks ago, the one gotcha in the authentication middleware that will silently break if you touch it: gone. The agent does not know any of it unless you tell it again.&lt;br&gt;
The fix is a &lt;code&gt;CLAUDE.md&lt;/code&gt; file in the root of every project. Claude Code reads this file automatically at the start of each session, which means it walks into the codebase already briefed: how the project is structured, what conventions to follow, what not to touch, and why certain decisions were made. It is the difference between starting a session with a junior developer who has never seen your codebase and starting a session with one who was briefed before they arrived. I include things like folder structure, key design decisions, known gotchas, and any non-obvious constraints. What is not in that file will surface at the worst possible time, usually when the agent is three files deep into something it should not have started. I have learned this lesson more than once.&lt;br&gt;
There are actually two levels of CLAUDE.md worth maintaining separately. The per-project file, described above, carries context specific to that codebase. But there is also a global CLAUDE.md at &lt;code&gt;~/CLAUDE.md&lt;/code&gt; that Claude Code reads across every session, regardless of project. This is where the universal stuff lives: how you like responses formatted, code style preferences that never change, recurring patterns you reach for project after project. The smartest way I have found to populate it: ask Claude directly. Open a session after you have done a few projects and ask it what it has noticed you doing repeatedly across different codebases — preferences, habits, corrections you keep making. The answer is usually more accurate than anything you would write from memory, and it goes straight into the global file. You only have to do that calibration once, and every future session inherits it.&lt;br&gt;
The same logic of "define it once, reuse it forever" applies to custom slash commands. Claude Code lets you create your own &lt;code&gt;/commands&lt;/code&gt; by dropping a markdown file into &lt;code&gt;.claude/commands/&lt;/code&gt; inside a project, or into &lt;code&gt;~/.claude/commands/&lt;/code&gt; for commands that travel with you globally. Anything you find yourself prompting repeatedly is a candidate: I have a &lt;code&gt;/pr&lt;/code&gt; command that opens a pull request with a consistent format, a &lt;code&gt;/review&lt;/code&gt; command that runs a code review against a checklist I care about, and a &lt;code&gt;/standup&lt;/code&gt; command that summarizes what changed since the last commit in plain language. The rule of thumb is the same as for skills: if you have typed the same instruction more than twice, it should be a command. The overhead is a single markdown file; the return is that you never type it again.&lt;br&gt;
In practice, this shapes how I move between tasks. One session handles one specific thing, 95% of the time. When that task is done and I want to start something different, I update the CLAUDE.md first, capturing any decisions made, gotchas discovered, or context the next session will need, and then launch fresh. This is not a workaround; it is the actual workflow. Each session stays focused on exactly one thing, the context window never accumulates unrelated baggage, and the token cost stays predictable because the session ends when the task ends.&lt;br&gt;
One thing I want to push back on slightly: the idea that more persistent memory is always better. The "second brain" framing — building an ever-growing knowledge base that carries everything forward — is appealing in theory, but I have found clean starts genuinely valuable. A fresh session with a tight, well-written CLAUDE.md is often sharper and more focused than a long session carrying the accumulated noise of everything that came before. Starting fresh is not a disadvantage; sometimes it is the whole point. This is especially true when starting a brand new project: no CLAUDE.md, no prior context, no assumptions inherited from a different codebase. The agent approaches it with the same clean slate you do, which means nothing from the last project bleeds into this one. That is not a limitation of the tool; it is the right default. The CLAUDE.md approach hits the balance I actually want: enough persistent context to orient the agent quickly, without the clutter.&lt;/p&gt;
&lt;h2&gt;
  
  
  When the context window fills up
&lt;/h2&gt;

&lt;p&gt;Agentic coding sessions on complex tasks will, eventually, produce a context window that is full or close to it. The agent's effective memory degrades as the window saturates, and you start getting responses that feel slightly off, repetitive, or strangely overconfident about something it got wrong two thousand tokens ago.&lt;br&gt;
Claude Code handles this with automatic context compaction, which summarizes earlier parts of the conversation to make room. For sessions where I want manual control, I use the &lt;code&gt;/compact&lt;/code&gt; command to trigger a summary on demand. When even that is not enough, the bluntest tool available is the right one: start a fresh session, point at the &lt;code&gt;CLAUDE.md&lt;/code&gt; file, and re-brief the agent on exactly where the previous session left off. It feels a bit caveman, but a focused, fresh session outperforms a saturated long one almost every time. My experience is that a 10,000-token focused session produces better output than a 100,000-token sprawling one that has lost the thread.&lt;/p&gt;
&lt;h2&gt;
  
  
  Web scraping: where the Zyte layer comes in
&lt;/h2&gt;

&lt;p&gt;Web data is not optional for serious agentic workflows. Agents that can research, verify facts, monitor changes, track competitors, or enrich datasets with live information are dramatically more useful than agents working from static knowledge alone. The web is the data source.&lt;br&gt;
The problem is that the web does not cooperate equally. Some pages render entirely in JavaScript and return nothing useful to a basic HTTP request. Others sit behind rate limits, bot detection, or login walls. Some block entire cloud IP ranges outright. An agent that tries to fetch a page and gets a 403, a CAPTCHA, or a JavaScript shell with no content is effectively blind, and it will usually not tell you that clearly; it will just work with whatever it got. For the straightforward cases, Claude's built-in browsing is fine. For anything beyond that, you need a layer that actually understands how modern websites are built and how to get through them reliably.&lt;br&gt;
That is where Zyte's tooling earns its place. For web scraping work, the setup picks up a layer specific to Zyte's tooling. Zyte publishes an official set of Claude Code skills at &lt;a href="https://github.com/zyte-ai/claude-skills" rel="noopener noreferrer"&gt;github.com/zyte-ai/claude-skills&lt;/a&gt;, installable in two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add zyte-ai/claude-skills
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;zyte-web-data@zyte-ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, the skills slot into Claude Code as slash commands and activate automatically on relevant prompts. The ones I reach for most are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/scrape&lt;/code&gt;: end-to-end workflow from a URL to a working &lt;a href="https://scrapy.org" rel="noopener noreferrer"&gt;Scrapy&lt;/a&gt; spider with web-poet page objects; this is the one you use when you just want to hand Claude a URL and a description of what to extract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/scrape-define&lt;/code&gt;: downloads a single detail page, discovers extractable fields, and iterates on the schema in the terminal until you approve it; good for quickly scoping what a site can give you&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/scrape-explore-site&lt;/code&gt;: crawls from a start URL and saves a diverse set of pages (start, list, and detail) with classified links; useful before committing to a schema&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/scrape-codegen&lt;/code&gt;: takes an extraction spec and generates the web-poet page object code; the output of &lt;code&gt;/scrape-define&lt;/code&gt; feeds directly into this&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/scrape-scrapy-cloud&lt;/code&gt;: deploys projects, schedules spiders, manages jobs, and surfaces logs and items from &lt;a href="https://www.zyte.com/scrapy-cloud/" rel="noopener noreferrer"&gt;Scrapy Cloud&lt;/a&gt;, all from the terminal
The skills integrate with the &lt;a href="https://www.zyte.com/web-scraping-copilot/" rel="noopener noreferrer"&gt;Web Scraping Copilot&lt;/a&gt; and are designed to pick up scraping prompts automatically, so you do not need to invoke a specific command for routine requests. If you are curious how these fit into a broader workflow, the post on &lt;a href="https://www.zyte.com/blog/supercharging-web-scraping-with-claude-skills" rel="noopener noreferrer"&gt;supercharging web scraping with Claude skills&lt;/a&gt; covers the combination in detail.
Everything is git-tracked, including personal side projects. The &lt;code&gt;ghs&lt;/code&gt; alias in my shell switches git identity instantly between my Zyte work email and my personal email, which eliminates the risk of pushing to the wrong remote after a context switch between work and a side project.
On MCP servers versus CLI tools: my standing rule is to reach for a CLI tool first and add an MCP server only when there is genuinely no CLI equivalent. MCP servers add indirection between the agent and the tool, and that indirection is not free: it makes the toolchain harder to audit, harder to debug, and slightly more likely to produce ambiguous outputs. If you are weighing your options, the &lt;a href="https://www.zyte.com/blog/claude-skills-vs-mcp-vs-web-scraping-copilot" rel="noopener noreferrer"&gt;comparison of Claude skills, MCP, and Web Scraping Copilot&lt;/a&gt; is worth reading before committing.
One area where I have been rethinking the default recently is web search. Most agents fall back to keyword-based search, which is fine for locating a documentation page but falls apart when an agent needs to do actual research. I came across &lt;a href="https://exa.ai" rel="noopener noreferrer"&gt;Exa&lt;/a&gt; at a local developer meetup, and it is built specifically for AI agents using semantic search rather than keyword matching, which produces noticeably better results when the agent needs to find conceptually related content rather than an exact phrase. The catch, and the reason I have not fully switched over, is that Exa currently only offers an MCP server and not a CLI utility. That puts it in direct conflict with the CLI-first rule: every time the agent invokes an MCP server there is a context switch, a round-trip, and a small but real cost in time and tokens that adds up over a long session. So for now I enable Exa selectively on projects where deep research is a core part of the work, and fall back to Claude's built-in search everywhere else. I am still exploring it, and if a CLI lands I will probably use it much more broadly.
The last piece of the scraping layer is what I think of as the objective metric loop. Before running the agent on a scraping task, I define a concrete, measurable target: field fill rate above 95%, zero extraction errors across 100 test URLs, or a specific field-level accuracy requirement. The agent runs, the output is evaluated automatically against that metric, and I re-prompt with the delta. The loop continues until the metric is hit, not until the code looks right on inspection. "Looks right" is not a metric.
## A few principles, for what they are worth
These are not best practices from a blog post. They are things I arrived at through repetition, usually after doing the opposite first.
&lt;strong&gt;Stop obsessing over prompts.&lt;/strong&gt; Models are meaningfully smarter than they were twelve months ago, and they will be smarter again in twelve more. A clear, complete description of what you want is almost always sufficient today. Intricate prompt engineering made more sense when models were brittle; spending that energy on your workflow instead will compound better.
&lt;strong&gt;Anything done twice should become a skill.&lt;/strong&gt; If you have guided an agent through the same process more than once, it belongs in a skill file. A skill is a reusable, well-described prompt with clear inputs and outputs. The overhead of writing one is low; the compounding return is not.
&lt;strong&gt;Each skill should do exactly one thing.&lt;/strong&gt; A skill that researches a topic, writes a script, and suggests titles is three skills waiting to be separated. Single-purpose skills are easier to debug, easier to improve, and much easier to reason about when something breaks.
&lt;strong&gt;Skills can be chained into workflows.&lt;/strong&gt; Three separate skills (research the next video topic, write a script from the research, suggest titles from the script) can be combined in sequence to produce a full workflow while remaining individually useful and testable. The composition is more flexible than a monolith, and any one skill can be swapped out without rebuilding everything.
&lt;strong&gt;Bundle custom scripts with the skills that need them.&lt;/strong&gt; If a skill depends on a helper script (a parser, a formatter, a validator), keep it in the same directory. Skills that rely on tools scattered elsewhere become fragile. Skills that travel with their dependencies stay portable.
## The rest of the bench
Not everything in my setup is fully integrated or daily-use. A few tools I keep within reach at different stages of exploration:
&lt;strong&gt;ChatGPT&lt;/strong&gt; (GPT 4.5 and above) is where I go for conversational research: thinking through a problem in plain language, getting a second opinion on an approach before committing to it in code, or just having a broad discussion that would clog an agentic workflow. Not everything needs an agent.
&lt;strong&gt;Perplexity&lt;/strong&gt; covers manual search and research where I want cited sources rather than a generated answer. I am also currently poking at Perplexity Computer, though it is genuinely early days and I do not have a settled opinion on it yet.
&lt;strong&gt;Local LLMs&lt;/strong&gt; via LM Studio and Ollama, used for offline experimentation. I will be honest: my current hardware is the constraint, not the tooling. Running anything genuinely capable locally is a stretch on my machine. If you are in the same position and want to know what you can actually run before committing to a download, &lt;a href="https://llmfit.com" rel="noopener noreferrer"&gt;LLMFit&lt;/a&gt; is a handy utility that evaluates your system specs and tells you which models are feasible, and worth running before you spend an afternoon downloading a 70B model that will not fit in your RAM.
## Pick one thing
The setup works because of the discipline behind it, not the tools themselves: plan before executing, give agents only the permissions they actually need, write the &lt;code&gt;CLAUDE.md&lt;/code&gt; file before you need it (not after), evaluate against metrics rather than impressions, and restart aggressively when the context window is saturated. Most of what I have described here is free or pay-as-you-go, and none of it requires a large upfront commitment to try.
If you are coming to this fresh, pick one piece rather than the whole stack. Enforcing plan mode before every task will return more value more quickly than any new tool installation, and adding a &lt;code&gt;CLAUDE.md&lt;/code&gt; to a project you already work in will pay off in the first session. If you work with Scrapy and want to add the web scraping layer that connects this setup to Zyte's toolchain, the &lt;a href="https://app.zyte.com/account/signup/zyteapi" rel="noopener noreferrer"&gt;Zyte free trial&lt;/a&gt; is where to start.
There is more to cover — the Karpathy metric loop in more depth, how I use CLAUDE.md across different project types, and how the local LLM setup is evolving as hardware catches up. If any of that sounds worth a Part 2, let me know in the comments. And if you want to stay across what the team at Zyte is building, &lt;a href="https://www.zyte.com/blog/" rel="noopener noreferrer"&gt;subscribing to the Zyte newsletter&lt;/a&gt; means you will not miss it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claude</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Stop using Python `requests` for web scraping: there are better &amp; modern libraries instead</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Thu, 09 Apr 2026 11:09:31 +0000</pubDate>
      <link>https://dev.to/extractdata/stop-using-python-requests-for-web-scraping-there-are-better-modern-libraries-instead-500d</link>
      <guid>https://dev.to/extractdata/stop-using-python-requests-for-web-scraping-there-are-better-modern-libraries-instead-500d</guid>
      <description>&lt;p&gt;While the 'Requests' library remains the default choice for many Python developers due to its reliability and extensive documentation, the Python HTTP landscape has evolved considerably. &lt;/p&gt;

&lt;p&gt;Modern alternatives now offer significant advantages, including built-in asynchronous support, HTTP/2 compatibility, enhanced performance, and up-to-date TLS handling. &lt;/p&gt;

&lt;p&gt;This article introduces and compares three such contemporary clients: HTTPX, curl_cffi, and rnet, detailing their unique features and practical applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with Requests for web scraping
&lt;/h2&gt;

&lt;p&gt;It's important to clarify Requests' limitations before proceeding; for simple API interactions with well-behaved endpoints, it still remains the de facto standard.&lt;/p&gt;

&lt;p&gt;However, a major drawback of the Requests library when it comes to web scraping is its predictable HTTP client fingerprint. This fingerprint, a unique combination of TLS version, cipher suites, HTTP headers, and connection characteristics, is sent with every request, and is well-known and cataloged by anti-bot systems. &lt;/p&gt;

&lt;p&gt;Consequently, if you're interacting with any endpoint, including APIs or services protected by anti-ban vendors, your request can be blocked purely based on &lt;em&gt;how&lt;/em&gt; the &lt;code&gt;requests&lt;/code&gt; library identifies itself. This happens even &lt;em&gt;before&lt;/em&gt; your credentials or payload are scrutinized, highlighting a significant limitation when targeting systems that perform client-side validation.&lt;/p&gt;

&lt;p&gt;In addition to issues like fingerprinting, a major limitation of the &lt;code&gt;requests&lt;/code&gt; library is its lack of native asynchronous support. This absence of async capability is particularly problematic when handling workloads that involve numerous HTTP &lt;code&gt;requests&lt;/code&gt;. Without it, the calls execute sequentially, and the program's thread remains blocked for the entire duration of each individual request.&lt;/p&gt;

&lt;p&gt;For straightforward scenarios, the standard &lt;code&gt;requests&lt;/code&gt; API call remains perfectly functional, as demonstrated in a quick example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://jsonplaceholder.typicode.com/posts/1&lt;/span&gt;&lt;span class="sh"&gt;"&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;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean and simple. For a one-off call to a standard REST API, this is fine. The gaps start showing when you need concurrency, HTTP/2, or when the target endpoint does any kind of client validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the Alternatives
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;httpx       or  uv add https
pip &lt;span class="nb"&gt;install &lt;/span&gt;curl-cffi       or  uv add curl-cffi
pip &lt;span class="nb"&gt;install &lt;/span&gt;rnet        or  uv add rnet &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                    uv add asyncio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. HTTPX
&lt;/h2&gt;

&lt;p&gt;HTTPX is the most direct upgrade from Requests as the API is nearly identical. If you know Requests, you already know most of HTTPX. What it adds is first-class async support, HTTP/2, and a more modern internal architecture.&lt;/p&gt;

&lt;p&gt;Where it differs from Requests is the explicit use of a &lt;code&gt;Client&lt;/code&gt; context manager (strongly recommended over module-level function calls) and the &lt;code&gt;AsyncClient&lt;/code&gt; for async usage. This gives you connection pooling and proper resource cleanup by default.&lt;/p&gt;

&lt;p&gt;HTTPX is the right starting point if you're looking for a migration that requires minimal code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Sync
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&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="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://jsonplaceholder.typicode.com/posts/1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: Async (calling the Zyte API)
&lt;/h3&gt;

&lt;p&gt;Async is where HTTPX really earns its keep. Here it's used to fire multiple requests to the Zyte API concurrently, each request blocks on the server side until extraction is complete, but your event loop stays free to send others in parallel:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ZYTE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ENDPOINT&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://api.zyte.com/v1/extract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://httpbin.org&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ENDPOINT&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browserHtml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&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;response&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&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="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&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;—&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browserHtml&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;chars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;raise_for_status()&lt;/code&gt; raises &lt;code&gt;httpx.HTTPStatusError&lt;/code&gt; on 4xx/5xx responses.
&lt;/li&gt;
&lt;li&gt;HTTP/2 support requires &lt;code&gt;pip install httpx[http2]&lt;/code&gt; and passing &lt;code&gt;http2=True&lt;/code&gt; to the client.
&lt;/li&gt;
&lt;li&gt;The 60-second timeout accounts for the Zyte API's server-side blocking behavior — it holds the connection open until extraction completes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. curl_cffi
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl_cffi&lt;/code&gt; wraps &lt;code&gt;libcurl&lt;/code&gt; with Python bindings and adds something HTTPX doesn't have: TLS fingerprint impersonation. It can show the TLS handshake of Chrome, Firefox, Safari, and other browsers. For API calls hitting endpoints protected by anti-ban or similar systems, this can be the difference between getting a response and getting a 403.&lt;/p&gt;

&lt;p&gt;The interface closely mirrors Requests, with the addition of the impersonate parameter. It supports both sync and async usage. For most API calls where fingerprinting isn't a concern, curl_cffi behaves just like Requests, the impersonate parameter is opt-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Sync
&lt;/h3&gt;



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

&lt;span class="n"&gt;response&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://jsonplaceholder.typicode.com/posts/1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;impersonate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chrome&lt;/span&gt;&lt;span class="sh"&gt;"&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;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: Async (calling the Zyte API)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;curl_cffi.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ZYTE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ENDPOINT&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://api.zyte.com/v1/extract&lt;/span&gt;&lt;span class="sh"&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;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&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;browserHtml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_zyte_api&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;impersonate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chrome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&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;ENDPOINT&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;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&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;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&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;—&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browserHtml&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;chars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;call_zyte_api&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;impersonate="chrome"&lt;/code&gt; sends Chrome's TLS fingerprint on every request made through this session.
&lt;/li&gt;
&lt;li&gt;Other supported values include &lt;code&gt;"firefox", "safari", "chrome110"&lt;/code&gt;, and more — check the &lt;code&gt;curl-cffi&lt;/code&gt; docs for the full list.
&lt;/li&gt;
&lt;li&gt;The sync interface (&lt;code&gt;from curl_cffi import requests&lt;/code&gt;) is nearly identical to the &lt;code&gt;requests&lt;/code&gt; module, making it the easiest drop-in if you only need sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. rnet
&lt;/h2&gt;

&lt;p&gt;rnet is the newest of the three. Like a lot of modern Python, it's built on Rust, making it async-first and performance-oriented. Like curl_cffi, it supports TLS impersonation, but its primary differentiator is throughput. It is designed for high-concurrency workloads where you're firing many requests simultaneously.&lt;/p&gt;

&lt;p&gt;The API surface is different from Requests, so it's not a drop-in replacement. But the patterns are clean and modern, and for async-heavy workloads it's worth the minor adjustment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Sample library code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rnet&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Impersonate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Build a client
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;impersonate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Impersonate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Firefox139&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Use the API you're already familiar with
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://tls.peet.ws/api/all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Print the response
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&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="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rnet&lt;/code&gt; is async-first; sync support is limited.
&lt;/li&gt;
&lt;li&gt;Response body methods like .json() and .text() are awaitable.
&lt;/li&gt;
&lt;li&gt;The Rust core makes it particularly well-suited for high-throughput concurrent workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;HTTPX&lt;/th&gt;
&lt;th&gt;curl_cffi&lt;/th&gt;
&lt;th&gt;rnet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sync Support&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&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;Async support&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes (primary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ With extra dependencies&lt;/td&gt;
&lt;td&gt;✅ Via libcurl&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good–High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS changes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;p&gt;Use Requests for simple, one-off scripts, internal tooling, or any situation where you're hitting a cooperative API endpoint and don't need concurrency. Nothing wrong with it in that context.&lt;/p&gt;

&lt;p&gt;Use HTTPX when you need async, want the closest migration path from Requests, or need HTTP/2. It's the safest default upgrade for most projects.&lt;/p&gt;

&lt;p&gt;Use curl_cffi when TLS fingerprint control matters, whether that's because you're hitting an anti-ban wall or an API with strict client validation, or any service that checks how a client identifies itself at the TLS layer.&lt;/p&gt;

&lt;p&gt;Use rnet when raw async performance is the priority. Its Rust foundation makes it the strongest choice for high-concurrency workloads where you're firing many requests simultaneously and need low overhead.&lt;/p&gt;

&lt;p&gt;The optimal choice is determined by several factors: your concurrency requirements, the target endpoint's sensitivity to client identification, and the desired similarity between the new code and your existing &lt;code&gt;requests&lt;/code&gt; implementation.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Small models, big ideas: what Google Gemma and MoE mean for developers</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Tue, 07 Apr 2026 12:50:03 +0000</pubDate>
      <link>https://dev.to/extractdata/small-models-big-ideas-what-google-gemma-and-moe-mean-for-developers-3038</link>
      <guid>https://dev.to/extractdata/small-models-big-ideas-what-google-gemma-and-moe-mean-for-developers-3038</guid>
      <description>&lt;p&gt;We at zyte-devrel try to stay plugged into what is happening in the AI and developer tooling space, not just because it is interesting, but because a lot of it starts having real implications for how we build and think about web data pipelines. Lately, one development that has had us genuinely curious is Google's new &lt;a href="https://deepmind.google/models/gemma/" rel="noopener noreferrer"&gt;Gemma 4&lt;/a&gt; model family, and specifically the direction it points toward with Mixture of Experts (MoE) architecture.&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.amazonaws.com%2Fuploads%2Farticles%2Ft3g8mot3kvpvdj01sf8s.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.amazonaws.com%2Fuploads%2Farticles%2Ft3g8mot3kvpvdj01sf8s.jpg" alt="IGemma 4 on iPhone" width="800" height="1517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not a deep tutorial. It is more of a "hey, here is what we have been poking at" - the kind of update we would share in a Slack channel or over coffee. If you wanna participate in such discussions, our &lt;a href="https://discord.com/invite/DwTnbrm83s" rel="noopener noreferrer"&gt;discord is always a welcoming platform&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is Gemma 4?
&lt;/h2&gt;

&lt;p&gt;Gemma has been dubbed as stripped down versions of Google Gemini. The new Gemma 4 is Google's latest family of open-weight language models, released last week. The lineup covers four sizes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2B&lt;/strong&gt;: ultra-efficient, built for mobile and edge devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4B&lt;/strong&gt;: enhanced multimodal capabilities, still edge-deployable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;26B&lt;/strong&gt;: sparse model using Mixture of Experts architecture (more on this below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;31B&lt;/strong&gt;: dense model for more demanding tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four variants support multimodal input (text and images), over 140 languages, a 128K-256K token context window, and agentic workflows with tool use and JSON output. The 2B and 4B models are specifically designed to run fully offline on modern edge devices like smartphones, with no internet dependency at all.&lt;/p&gt;

&lt;p&gt;According to Google's &lt;a href="https://deepmind.google/models/gemma/" rel="noopener noreferrer"&gt;Gemma 4 model page&lt;/a&gt;, the family ranks third among open-weighted models on the LM Arena leaderboard and uses 2.5 times fewer tokens than comparable models for equivalent tasks. &lt;/p&gt;

&lt;p&gt;The Gemma 4 26B MoE, specially caught my attention because unlike other variants it's based on MoE architecture and it does make a difference :&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MoE, and why does it matter?
&lt;/h2&gt;

&lt;p&gt;Mixture of Experts (MoE) is one of those ideas that sounds complex but is actually pretty intuitive once you hear the analogy.&lt;/p&gt;

&lt;p&gt;In a traditional dense neural network, every parameter in the model activates for every input. It is like calling your entire company into a meeting every time someone has a question. It works, but it is expensive.&lt;/p&gt;

&lt;p&gt;MoE works differently. Instead of one large model doing everything, you have a set of smaller "expert" sub-networks, each specialized in different patterns, plus a router that looks at each incoming token and decides which one or two experts to activate. Most of the model sits idle at any given moment.&lt;/p&gt;

&lt;p&gt;The result: you get the quality of a much larger model at a fraction of the inference cost.&lt;/p&gt;

&lt;p&gt;The Gemma 4 26B model is a great illustration of this. It has 26 billion total parameters, but during inference it only activates around 3.8 billion of them. You get near-26B quality at roughly 3.8B compute cost. That is the MoE advantage in one number.&lt;/p&gt;

&lt;p&gt;Other models that take the same approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixtral 8x7B&lt;/strong&gt;: eight experts, two active per token; it &lt;a href="https://huggingface.co/blog/moe" rel="noopener noreferrer"&gt;outperforms Llama 2 70B&lt;/a&gt; on most benchmarks at far lower inference cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kimi&lt;/strong&gt;: Moonshot AI's model, also MoE-based, has been making similar waves in the open-model space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a deep dive on how MoE works under the hood, the &lt;a href="https://huggingface.co/blog/moe" rel="noopener noreferrer"&gt;Hugging Face guide to mixture of experts&lt;/a&gt; is well worth the read.&lt;/p&gt;

&lt;p&gt;Since the models are free, if you have the right machine you can host them lcoally using &lt;a href="https://ollama.com/library/gemma4" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; or call them using API services like &lt;a href="https://openrouter.ai/google/gemma-4-26b-a4b-it" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;My prefered way of using a new mode is through &lt;a href="https://claude.ai/login" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, but I believe Gemma4 has a different tool calling structure so it is not compatible yet, but you can use it with &lt;a href="https://lmstudio.ai/models/gemma-4" rel="noopener noreferrer"&gt;LMStudio&lt;/a&gt;, or skil all that because you can now&lt;/p&gt;

&lt;h2&gt;
  
  
  Run Gemma 4 offline on an iPhone
&lt;/h2&gt;

&lt;p&gt;Here is the part worth sharing, because it genuinely surprised us.&lt;/p&gt;

&lt;p&gt;Using the &lt;a href="https://apps.apple.com/us/app/google-ai-edge-gallery/id6749645337" rel="noopener noreferrer"&gt;Google Edge AI Gallery app&lt;/a&gt; from the App Store, you can load a Gemma 4 model and run it with airplane mode on. No API calls, no cloud round-trips, no data leaving the device. Just the model running locally on your phone.&lt;/p&gt;

&lt;p&gt;The experience is not going to replace a foundational frontier model for complex reasoning. But that is not the point. For quick classification, summarization, or just experimenting with local inference, the 2B and 4B variants are remarkably capable, and there are zero API costs with no data leaving your device. And since it is multi-modal you can practically point your phone camera to a paper recipt and ask it to save the details in a spreadhseet. &lt;/p&gt;

&lt;p&gt;If you have not tried running a local large language model (LLM) yet, this is probably the lowest-friction entry point on hardware you already own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should developers building data pipelines care?
&lt;/h2&gt;

&lt;p&gt;Here is where it connects back to what a lot of us are building.&lt;/p&gt;

&lt;p&gt;When LLMs run on-device or at the edge, the calculus around data pipelines shifts in a few useful directions:&lt;/p&gt;

&lt;p&gt;Tokens are getting expensive and when a model as good as Gemma 4 or Qwen-3.5 is free and open-weighted it's a welcome development. Everyone's complaining about running out of their claude usage quota last couple of weeks or getting huge bills, thanks to giving Opus API Keys to OpenClaw. These things can be significantly addressed using Open Models.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No API round-trips&lt;/strong&gt;: on-device inference eliminates latency from cloud API calls. For classification tasks running inside a scraping pipeline, this is a meaningful difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data privacy&lt;/strong&gt;: running extraction locally means scraped content never leaves your infrastructure. For regulated industries or sensitive datasets, that is a significant advantage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost at scale&lt;/strong&gt;: if you are doing high-volume classification — is this a product page? is this content in the target language? — running a small local model beats paying per-token at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge preprocessing&lt;/strong&gt;: a small LLM can filter and classify pages before they ever reach a more expensive cloud model for deeper analysis, and I am personally looking forward to run them on SBCs like a Raspberry Pi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Weights&lt;/strong&gt;: people often confuse open-weights models with open-source models, while the lines may be blurry and even I don't fully understand the difference, one thing I know for sure is that Gemma 4 is available under the Apache 2.0 license, which allows building and selling products on top of it and open-weights allows you to fine-tune it for your use-case or application. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's me playing it with it on my iPhone 16, completely offline:&lt;br&gt;
  &lt;/p&gt;
&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/V88iUYQd4BU"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;h2&gt;
  
  
  Just checking in
&lt;/h2&gt;

&lt;p&gt;We do not have grand proclamations here. This is a space that is moving fast, and we are learning alongside everyone else.&lt;/p&gt;

&lt;p&gt;If you have been experimenting with local LLMs in your scraping or data extraction workflows, we would genuinely love to hear about it. Drop a comment below, or find us on the &lt;a href="https://discord.com/invite/DwTnbrm83s" rel="noopener noreferrer"&gt;Zyte discord&lt;/a&gt; and read more interesting blogs on &lt;a href="https://www.zyte.com/blog/" rel="noopener noreferrer"&gt;Zyte Blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to try this yourself, here are three good starting points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/google-ai-edge-gallery/id6749645337" rel="noopener noreferrer"&gt;Google Edge Gallery&lt;/a&gt;: available on the App Store and Playstore, runs Gemma 4 locally on iOS&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/google/gemma" rel="noopener noreferrer"&gt;Gemma models on Hugging Face&lt;/a&gt;: for running on desktop or server&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://deepmind.google/models/gemma/" rel="noopener noreferrer"&gt;Google's Gemma 4 model page&lt;/a&gt;: full family overview, benchmarks, and architecture details&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>google</category>
      <category>llm</category>
      <category>news</category>
    </item>
    <item>
      <title>Headless web exploration is the way to go!</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Mon, 16 Mar 2026 09:21:25 +0000</pubDate>
      <link>https://dev.to/iayanpahwa/headless-web-exploration-is-the-way-to-go-3kim</link>
      <guid>https://dev.to/iayanpahwa/headless-web-exploration-is-the-way-to-go-3kim</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/extractdata" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11159%2F9b0ab14b-3550-4e5e-b996-02b33c0912fa.jpg" alt="Extract by Zyte" width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F873075%2F4be7029b-bbfa-4a1e-8090-9e7e561d96a0.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/extractdata/i-built-a-claude-code-skill-that-screenshots-any-website-and-it-handles-anti-bot-sites-too-2m4b" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;I built a Claude Code skill that screenshots any website (and it handles anti-bot sites too)&lt;/h2&gt;
      &lt;h3&gt;Ayan Pahwa for Extract by Zyte ・ Mar 6&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#claude&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webscraping&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#python&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#ai&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>claude</category>
      <category>webscraping</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a Claude Code skill that screenshots any website (and it handles anti-bot sites too)</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Fri, 06 Mar 2026 14:40:32 +0000</pubDate>
      <link>https://dev.to/extractdata/i-built-a-claude-code-skill-that-screenshots-any-website-and-it-handles-anti-bot-sites-too-2m4b</link>
      <guid>https://dev.to/extractdata/i-built-a-claude-code-skill-that-screenshots-any-website-and-it-handles-anti-bot-sites-too-2m4b</guid>
      <description>&lt;p&gt;TLDR;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Automate screenshot capture for any URL with JavaScript rendering and anti-ban protection — straight from your AI assistant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/P2HhnFRXm-I"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Taking a screenshot of a webpage sounds trivial, until you need to do it at scale. Modern websites throw every obstacle imaginable in your way: JavaScript-rendered content that only appears after a React bundle loads, bot-detection systems that serve blank pages to automated headless browsers, geo-blocked content, and CAPTCHAs that appear the moment traffic patterns look non-human. For a handful of URLs you can get away with Puppeteer or Playwright. For hundreds or thousands? You need infrastructure built for the job.&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.amazonaws.com%2Fuploads%2Farticles%2Fnca4lwviajb7vavqwio5.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.amazonaws.com%2Fuploads%2Farticles%2Fnca4lwviajb7vavqwio5.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Zyte API was designed specifically for this problem. It handles JavaScript rendering, anti-bot fingerprinting, rotating proxies, and headless browser management so you don't have to and what better way to do it straight from your LLM supplying the URLs? Hence I created this zyte-screenshots Claude Skill, which you can use to trigger the entire workflow- API call, base64 decode, PNG save on your filesystem, all just by chatting with Claude.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll walk through exactly how the skill works, how to set it up, and how to use it to capture production-quality screenshots of any URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use the Zyte API for Screenshots?
&lt;/h2&gt;

&lt;p&gt;Before diving into the skill itself, it's worth understanding what makes the Zyte API uniquely suited to screenshot capture at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Full JavaScript Rendering
&lt;/h3&gt;

&lt;p&gt;Single-page applications built with React, Vue, Angular, or Next.js don't serve their content in the raw HTML response, they render it client-side after the page loads. Tools that capture the raw HTTP response will get a blank shell. Zyte's screenshot endpoint fires a real headless browser, waits for the DOM to fully settle, then captures the final rendered state.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Anti-Bot and Anti-Ban Protection
&lt;/h3&gt;

&lt;p&gt;Enterprise-grade sites use fingerprinting libraries to detect automation. They check TLS fingerprints, browser headers, canvas rendering patterns, mouse movement entropy, and dozens of other signals. Zyte's infrastructure is battle-tested to pass these checks so your screenshots won't return a "Access Denied" page.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Scale Without Infrastructure
&lt;/h3&gt;

&lt;p&gt;Managing a fleet of headless browser instances, proxy rotation, retries, and residential IP pools is a serious engineering investment. Zyte abstracts all of this into a single API call.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. One API, Any URL
&lt;/h3&gt;

&lt;p&gt;Whether the target is a static HTML page, a JS-heavy SPA, a behind-login dashboard (with session cookies), or a geo-restricted site, the same API call structure works. The skill you're about to install uses this endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.zyte.com/account/signup/zyteapi" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fh9a7qsf6b03h1rrfzw5g.png" alt="Zyte API signup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the zyte-screenshots Claude Skill?
&lt;/h2&gt;

&lt;p&gt;Claude Skills are reusable instruction packages that extend Claude's capabilities with domain-specific workflows. The &lt;strong&gt;zyte-screenshots&lt;/strong&gt; skill teaches Claude how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept a URL from the user in natural language&lt;/li&gt;
&lt;li&gt;Read the ZYTE_API_KEY environment variable&lt;/li&gt;
&lt;li&gt;Construct and execute the correct curl command against &lt;a href="https://api.zyte.com/v1/extract" rel="noopener noreferrer"&gt;https://api.zyte.com/v1/extract&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pipe the JSON response through jq and base64 --decode to produce a PNG file&lt;/li&gt;
&lt;li&gt;Derive a clean filename from the URL (e.g. &lt;a href="https://quotes.toscrape.com" rel="noopener noreferrer"&gt;https://quotes.toscrape.com&lt;/a&gt; becomes quotes.toscrape.png)&lt;/li&gt;
&lt;li&gt;Report the exact file path and describe what's visible in the screenshot in one sentence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, this means you can open Claude, say &lt;strong&gt;"screenshot &lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt;"&lt;/strong&gt;, and have a pixel-perfect PNG on your filesystem in seconds, no browser, no script, no Puppeteer config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before installing the skill, make sure you have the following:&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;curl&lt;/strong&gt;: Pre-installed on macOS and most Linux distributions. On Windows, use WSL or Git Bash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jq&lt;/strong&gt;: A lightweight JSON processor. Install via &lt;code&gt;brew install jq&lt;/code&gt; (macOS) or &lt;code&gt;sudo apt install jq&lt;/code&gt; (Ubuntu/Debian).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;base64&lt;/strong&gt;: Standard on all Unix-like systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude desktop app&lt;/strong&gt; with Skills support enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A Zyte API Key
&lt;/h3&gt;

&lt;p&gt;Sign up at &lt;a href="https://www.zyte.com/" rel="noopener noreferrer"&gt;zyte.com&lt;/a&gt; and navigate to your API credentials. The free tier includes enough credits to get started with testing. Copy your API key, you'll set it as an environment variable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro tip:&lt;/strong&gt; Set your ZYTE_API_KEY in your shell profile (~/.zshrc or ~/.bashrc) so it's always available: &lt;code&gt;export ZYTE_API_KEY="your_key_here"&lt;/code&gt; or pass it along your prompt&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Installing the Skill
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Download the Skill from GitHub
&lt;/h3&gt;

&lt;p&gt;The skill is open source and available at &lt;a href="https://github.com/apscrapes/claude-zyte-screenshots" rel="noopener noreferrer"&gt;github.com/apscrapes/claude-zyte-screenshots&lt;/a&gt;. Download the latest release ZIP from the repository's Releases page, or clone it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/apscrapes/claude-zyte-screenshots.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Import into Claude
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the Claude desktop app or go to Claude.ai&lt;/li&gt;
&lt;li&gt;Navigate to Settings → Skills&lt;/li&gt;
&lt;li&gt;Click Import Skill&lt;/li&gt;
&lt;li&gt;Select the SKILL.md file from the downloaded ZIP&lt;/li&gt;
&lt;li&gt;Claude will confirm the skill has been installed&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Set Your API Key
&lt;/h3&gt;

&lt;p&gt;In your terminal, export your Zyte API key or pass it along your prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYTE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_zyte_api_key_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this permanent across terminal sessions, add the export line to your &lt;code&gt;~/.zshrc&lt;/code&gt; or &lt;code&gt;~/.bashrc&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Skill: Step-by-Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Basic Screenshot
&lt;/h3&gt;

&lt;p&gt;Once the skill is installed and your API key is set, using it is as simple as asking Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Screenshot https://quotes.toscrape.com using zyte-screenshots skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify ZYTE_API_KEY is available in the environment&lt;/li&gt;
&lt;li&gt;Derive the output filename: quotes.toscrape.png&lt;/li&gt;
&lt;li&gt;Execute the following command:
&lt;/li&gt;
&lt;/ol&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; https://api.zyte.com/v1/extract &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZYTE_API_KEY&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://quotes.toscrape.com",
    "screenshot": true
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.screenshot'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; quotes.toscrape.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Verify the file is non-empty&lt;/li&gt;
&lt;li&gt;Open and inspect the image&lt;/li&gt;
&lt;li&gt;Return the file location and a one-line visual description&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example output from Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Screenshot captured!

📁 Location: /home/you/quotes.toscrape.png

🖼️  What's in it: A clean light-themed page listing inspirational quotes
   with author attributions and tag filters in the sidebar.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Under the Hood: How the API Call Works
&lt;/h2&gt;

&lt;p&gt;Let's break down the exact curl command the skill executes:&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; https://api.zyte.com/v1/extract &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZYTE_API_KEY&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://target-site.com",
    "screenshot": true
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.screenshot'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; output.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;curl -s&lt;/code&gt;&lt;/strong&gt; — Silent mode; suppresses progress output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-u "$ZYTE_API_KEY":&lt;/code&gt;&lt;/strong&gt; — HTTP Basic Auth. Zyte uses the API key as the username with an empty password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-H "Content-Type: application/json"&lt;/code&gt;&lt;/strong&gt; — Tells the API to expect a JSON body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-d '{...}'&lt;/code&gt;&lt;/strong&gt; — The JSON request body. Setting &lt;code&gt;screenshot: true&lt;/code&gt; instructs Zyte to return a base64-encoded PNG of the fully rendered page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;| jq -r '.screenshot'&lt;/code&gt;&lt;/strong&gt; — Extracts the raw base64 string from the JSON response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;| base64 --decode&lt;/code&gt;&lt;/strong&gt; — Decodes the base64 string into binary PNG data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;gt; output.png&lt;/code&gt;&lt;/strong&gt; — Writes the binary data to a PNG file.&lt;/p&gt;

&lt;p&gt;The Zyte API handles everything in between — spinning up a headless Chromium instance, loading the page with real browser fingerprints, waiting for JavaScript execution to complete, and rendering the final DOM to a pixel buffer.&lt;/p&gt;

&lt;p&gt;This was a fun weekend project I put together, let me know your thoughts on our Discord and feel free to play around with it. I'd also love to know if you create any useful claude skills or mcp server, so say hi on our discord.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: web scraping • Zyte API • screenshots at scale • JavaScript rendering • anti-bot • Claude AI • Claude Skills • automation • headless browser • site APIs&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>webscraping</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Raspberry Pi &amp; E-ink scrapes &amp; displays the price of Gold today</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Tue, 24 Feb 2026 08:39:31 +0000</pubDate>
      <link>https://dev.to/extractdata/how-i-trade-gold-using-e-ink-live-data-and-an-old-raspberry-pi-42ag</link>
      <guid>https://dev.to/extractdata/how-i-trade-gold-using-e-ink-live-data-and-an-old-raspberry-pi-42ag</guid>
      <description>&lt;p&gt;They say that “data is the new oil”, but there’s another hot commodity that’s setting markets alight - precious metals.&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.amazonaws.com%2Fuploads%2Farticles%2Frtbqu72m01w8auk29wfd.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.amazonaws.com%2Fuploads%2Farticles%2Frtbqu72m01w8auk29wfd.jpg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the last 12 months, the &lt;a href="https://uk.finance.yahoo.com/quote/GC%3DF/" rel="noopener noreferrer"&gt;value of gold&lt;/a&gt; has surged about 75%, while &lt;a href="https://www.gold.co.uk/silver-price/" rel="noopener noreferrer"&gt;silver has boomed&lt;/a&gt; more than 200%. That’s why I, like a growing number of others, now trade in the metal markets.&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.amazonaws.com%2Fuploads%2Farticles%2Fdanyckktjsbtvn1e2b3p.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.amazonaws.com%2Fuploads%2Farticles%2Fdanyckktjsbtvn1e2b3p.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These days, it is possible to buy &lt;em&gt;digital&lt;/em&gt; versions of precious metals. But I think of myself as a &lt;em&gt;collector&lt;/em&gt; - I like to buy &lt;em&gt;real&lt;/em&gt;, solid coins or bullions whenever I get a chance.&lt;/p&gt;

&lt;p&gt;In the last two years, I have acquired a small collection of gold bullions and silver coins, which have appreciated healthily. But I am not planning to sell and book a profit just yet. In fact, I want to buy more, especially when there’s a dip in the price.&lt;/p&gt;

&lt;p&gt;There’s just one problem that hits this hobby - prices of actual physical gold and silver bullions are very different in the retail market from stock exchanges’ spot prices and keeping track of them manually is cumbersome specially with a full time job.&lt;/p&gt;

&lt;p&gt;To take advantage of the dips and price arbitrage, I need to &lt;em&gt;automate&lt;/em&gt; my decisions. To buy gold old-style, I need a key resource from the modern trading toolset - data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn data into gold
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="http://gjc.org.in/" rel="noopener noreferrer"&gt;All-India Gem And Jewellery Domestic Council&lt;/a&gt; (GJC), a national trade federation for the promotion and growth of trade in gems and jewellery across, is the go-to site listing latest retail rates for gold and silver.&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.amazonaws.com%2Fuploads%2Farticles%2Fsgjjdai9hw0oyihva84n.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.amazonaws.com%2Fuploads%2Farticles%2Fsgjjdai9hw0oyihva84n.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alas, it doesn’t offer an API to access that data. But fear not - with web scraping skills and &lt;a href="https://www.zyte.com/zyte-api/" rel="noopener noreferrer"&gt;Zyte API&lt;/a&gt;, I can extract these prices quickly and regularly.&lt;/p&gt;

&lt;p&gt;And I can do it using some of the tech I love to tinker with.&lt;/p&gt;

&lt;p&gt;I call it &lt;a href="https://github.com/apscrapes/ExtractToInk" rel="noopener noreferrer"&gt;ExtractToInk&lt;/a&gt; - a custom project that pulls the latest prices on a two-inch, 250x122 e-ink display powered by a retired Raspberry Pi (total cost under US$50).&lt;/p&gt;

&lt;p&gt;This is the story of how I power my quest for rapid riches using cheap old hardware and the &lt;a href="https://www.zyte.com/blog/zyte-leads-proxyway-2025-web-scraping-api-report/" rel="noopener noreferrer"&gt;world’s best web scraping engine&lt;/a&gt; - and how you can, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mining for data
&lt;/h2&gt;

&lt;p&gt;Like many modern sites, GJC’s includes both JavaScript rendering for HTML and protection mechanisms- technologies that can break brittle traditional scraping solutions.&lt;/p&gt;

&lt;p&gt;This project connects all the dots:&lt;/p&gt;

&lt;p&gt;Web → Extract → Parse → Render → Physical display&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raspberry Pi (tested on Pi Zero 2 W), it should run on any Raspberry Pi Board
&lt;/li&gt;
&lt;li&gt;Pimoroni Inky pHAT (Black, SSD1608)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Software&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3
&lt;/li&gt;
&lt;li&gt;Zyte API: to get rendered HTML
&lt;/li&gt;
&lt;li&gt;BeautifulSoup: to parse HTML
&lt;/li&gt;
&lt;li&gt;Pillow and Inky Python libraries: for e-ink display stuff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let’s get building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Prepare hardware
&lt;/h2&gt;

&lt;p&gt;Setup your Raspberry Pi. In my case, I am using Raspberry Pi OS &lt;a href="https://www.raspberrypi.com/documentation/computers/getting-started.html" rel="noopener noreferrer"&gt;booted from the SD card&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.amazonaws.com%2Fuploads%2Farticles%2Furl2uiive3xweo4opha6.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.amazonaws.com%2Fuploads%2Farticles%2Furl2uiive3xweo4opha6.png" alt=" " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Depending on which display you use, it most probably will be connected to the Pi over i2c bus or SPI bus protocol - so, enable your display type by entering:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo raspi-config&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now attach your e-ink display and do a quick reboot &lt;/p&gt;

&lt;p&gt;You might need to install libraries to use your e-ink display.&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.amazonaws.com%2Fuploads%2Farticles%2Fkd77cowp2x5kj1hmvupf.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.amazonaws.com%2Fuploads%2Farticles%2Fkd77cowp2x5kj1hmvupf.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Fetching rendered HTML with Zyte API
&lt;/h2&gt;

&lt;p&gt;The source site, GJC, renders prices dynamically, using JavaScript - something which can make plain HTTP requests unreliable.&lt;/p&gt;

&lt;p&gt;No problem. By accessing the page through Zyte API, we can set &lt;code&gt;browserHTML&lt;/code&gt; mode to return the page content as though rendered in an actual browser.&lt;/p&gt;

&lt;p&gt;Instead of fighting JavaScript, we let Zyte handle it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;html = requests.post(`  
    `"https://api.zyte.com/v1/extract",`  
    `auth=(ZYTE_API_KEY, ""),`  
    `json={"url": URL, "browserHtml": True},`  
`).json()["browserHtml"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: there is no Selenium here, and no headless browsers. This is much more reliable for production-style scraping&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Parsing with CSS selectors
&lt;/h2&gt;

&lt;p&gt;Once we have clean HTML, parsing becomes straightforward.&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.amazonaws.com%2Fuploads%2Farticles%2Fzkwbp7ffd758ci8uu01r.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.amazonaws.com%2Fuploads%2Farticles%2Fzkwbp7ffd758ci8uu01r.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Gold prices
&lt;/h3&gt;

&lt;p&gt;Let’s locate the actual prices in the page mark-up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for row in soup.select(".gold_rate table tr"):`  
    `label = row.select_one("td strong")`  
    `values = row.select("td strong")`

    `if not label or len(values) &amp;lt; 2:`  
        `continue`

    `text = label.get_text(strip=True)`  
    `priceText = values[1].get_text()`

    `if "Standard Rate Buying" in text:`  
        `goldBuying = re.search(r"\d[\d,]*", priceText).group(0)`

    `if "Standard Rate Selling" in text:`  
        `goldSelling = re.search(r"\d[\d,]*", priceText).group(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re deliberately using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS selectors (easy to find from your browser’s DevTools).
&lt;/li&gt;
&lt;li&gt;Minimal regular expressions (only for numeric extraction).
&lt;/li&gt;
&lt;li&gt;Defensive checks to avoid brittle parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Silver prices
&lt;/h3&gt;

&lt;p&gt;Silver appears outside the main table, so we filter it carefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for strong in soup.select("p &amp;gt; strong"):`  
    `text = strong.get_text(" ", strip=True)`

    `if "Standard Rate Selling" in text and not strong.find_parent("table"):`  
        `silver = re.search(r"\d[\d,]*", text).group(0)`  
        `break
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Rendering for e-ink
&lt;/h2&gt;

&lt;p&gt;For this project, I did not want to pipe data into a web dashboard on a computer monitor.&lt;/p&gt;

&lt;p&gt;E-ink is always-on, low power, distraction-free and perfect for “ambient information” like this.&lt;/p&gt;

&lt;p&gt;So, it’s a great fit for data like prices, weather, status indicators and system health.&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.amazonaws.com%2Fuploads%2Farticles%2Fwp8mf7r0lr5iibk3h8b5.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.amazonaws.com%2Fuploads%2Farticles%2Fwp8mf7r0lr5iibk3h8b5.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But e-ink displays are not normal screens.&lt;/p&gt;

&lt;p&gt;They are typically black-and-white, have high contrast and are slow to refresh.&lt;/p&gt;

&lt;p&gt;What’s more, no two e-ink displays are made the same way. Every vendor has different support packages so, whichever you end up using, make sure to read the documentation and change the code accordingly.&lt;/p&gt;

&lt;p&gt;In my case, I am using &lt;a href="https://learn.pimoroni.com/article/getting-started-with-inky-phat" rel="noopener noreferrer"&gt;Pimoroni inky PHat&lt;/a&gt;. The supplied Python library has great built-in examples to get you quickly up and running. I used the helper function to render texts on the display, ex, the build in draw.text() function comes handy:&lt;/p&gt;

&lt;h3&gt;
  
  
  Draw silver selling price
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    draw.text((x, y), f"Silver : {silverPrice}", fill=(0, 0, 0), font=fontBig)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Taking it furtherSection about the finished product
&lt;/h2&gt;

&lt;p&gt;I built this project to use web data thoughtfully, connecting it to the physical world, and building pipelines that feel calm, reliable, and purposeful. When I am at my work-desk the project actively tells me the current prices so I can buy new coins if I see a price drop.&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.amazonaws.com%2Fuploads%2Farticles%2Fdf1n493o7cimspvfjrjw.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.amazonaws.com%2Fuploads%2Farticles%2Fdf1n493o7cimspvfjrjw.jpg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I can further extend this to place automatic orders on the website and secure me a coin at my desired strike price. &lt;/p&gt;

&lt;p&gt;If you want to take this further, you could also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run it via &lt;code&gt;cron&lt;/code&gt; every 10 minutes : The website I am targeting only refreshes prices twice a day, so my cron job runs every 12 hours, but, if you need faster data, you can scrape a site with more real-time updates.
&lt;/li&gt;
&lt;li&gt;Add more commodities or currencies.
&lt;/li&gt;
&lt;li&gt;Turn it into a &lt;code&gt;systemd&lt;/code&gt; service to run at start time.
&lt;/li&gt;
&lt;li&gt;Swap e-ink for another output (PDF, LED, dashboard).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re exploring Zyte API, or looking for real-world scraping examples beyond CSVs and JSON files, this project is a great place to start.&lt;/p&gt;

&lt;p&gt;You can get my code in the &lt;a href="https://github.com/apscrapes/ExtractToInk" rel="noopener noreferrer"&gt;ExtractToInk GitHub repository&lt;/a&gt; now.&lt;/p&gt;

</description>
      <category>python</category>
      <category>raspberrypi</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Holiday Gift Guide 2025: For Developers, Web Scrapers &amp; Everyone In Between</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Fri, 19 Dec 2025 07:55:12 +0000</pubDate>
      <link>https://dev.to/extractdata/holiday-gift-guide-2025-for-developers-web-scrapers-everyone-in-between-2hbp</link>
      <guid>https://dev.to/extractdata/holiday-gift-guide-2025-for-developers-web-scrapers-everyone-in-between-2hbp</guid>
      <description>&lt;p&gt;It’s that time of the year when the coffee gets stronger, commits get messier, and everyone agrees to finally refactor that script in January. And let’s be honest, most of us won’t. But while it lasts let’s celebrate the season of enjoying laid back family time and exchanging gifts. &lt;br&gt;
And to make gifting a little easier, we asked the Zyte team and community to share what they would love to receive. So if you’ve got a developer, a web scraper, or someone who just really enjoys arguing with APIs in your life… Here's your cheat sheet.&lt;br&gt;
Grab a hot drink, settle into your favourite debugging position, and let’s dive in. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer : It's not sponsored. All the recommendations are from community and author's personal experience using these products, hence no URL is provided but you should be able find these easily.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Book: Soul of a New Machine&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.amazonaws.com%2Fuploads%2Farticles%2Fhsk3748t6ioitfnv4qms.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.amazonaws.com%2Fuploads%2Farticles%2Fhsk3748t6ioitfnv4qms.png" alt=" " width="572" height="892"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the dev who loves origin stories. It’s a classic engineering tale that reminds us why we fell in love with building things in the first place. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Logitech MX Master mouse&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.amazonaws.com%2Fuploads%2Farticles%2Fbkn5l6tujar1d04fvqyf.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.amazonaws.com%2Fuploads%2Farticles%2Fbkn5l6tujar1d04fvqyf.png" alt=" " width="570" height="812"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Smooth scrolling. Perfect ergonomics. Side buttons that feel like cheat codes for productivity. This is the mouse equivalent of finally finding that one undocumented API endpoint you needed all year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. AeroPress + Coffee Subscription&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.amazonaws.com%2Fuploads%2Farticles%2Facfz2yudv63bxnq41916.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.amazonaws.com%2Fuploads%2Farticles%2Facfz2yudv63bxnq41916.png" alt=" " width="734" height="866"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If coffee is the real runtime powering your favourite developer, this combo is basically a performance upgrade. AeroPress means fast, clean brews; fresh beans mean they might actually fix that bug before lunch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Spider Plush Toy&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.amazonaws.com%2Fuploads%2Farticles%2Fltyo0r4rwnfq2ybtb482.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.amazonaws.com%2Fuploads%2Farticles%2Fltyo0r4rwnfq2ybtb482.png" alt=" " width="636" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For every web scraper who proudly identifies as “part human, part spider.” it sits silently on your desk reminding you to obey robots.txt (…most of the time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Git Merch Based on Their Commit History&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.amazonaws.com%2Fuploads%2Farticles%2Fdlexoulpxhdj1354vou5.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.amazonaws.com%2Fuploads%2Farticles%2Fdlexoulpxhdj1354vou5.png" alt=" " width="720" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A mug that says “I survived the merge conflict of 2025.”&lt;br&gt;
A T-shirt celebrating their 3,000-day streak.&lt;br&gt;
Or maybe a gentle reminder that “WIP” is not a personality.&lt;br&gt;
Funny, personal, and guaranteed to make them smile during standup.&lt;br&gt;
Link : &lt;a href="https://gitmerch.com/" rel="noopener noreferrer"&gt;https://gitmerch.com/&lt;/a&gt; (not-sponsored),  just enter their github username&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Nothing Headphones&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.amazonaws.com%2Fuploads%2Farticles%2Fxulp8u0vgwuj70i6dhli.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.amazonaws.com%2Fuploads%2Farticles%2Fxulp8u0vgwuj70i6dhli.png" alt=" " width="548" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sleek, transparent, and great for tuning out noisy offices or noisy families. Perfect for deep work, debugging, or pretending you can’t hear someone asking, “Can you take a quick look at this?”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Mechanical Keyboard (NuPhy / Keychron)&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.amazonaws.com%2Fuploads%2Farticles%2Fdwvejewr2qxjcsgilo9p.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.amazonaws.com%2Fuploads%2Farticles%2Fdwvejewr2qxjcsgilo9p.png" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Developers don’t just type on these; they perform.&lt;br&gt;
Clicky keys, gorgeous layouts, RGB that could guide aircraft, what’s not to love? Warning: once they switch, they’ll never stop talking about actuation force.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Bambu Lab A1 Mini 3D Printer&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.amazonaws.com%2Fuploads%2Farticles%2Fyve77kf7h4f7palr2hdt.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.amazonaws.com%2Fuploads%2Farticles%2Fyve77kf7h4f7palr2hdt.png" alt=" " width="540" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the dev who already has too many hobbies.&lt;br&gt;
They’ll print brackets, cable holders, figurines, replacement parts… and things no one can identify but everyone politely admires. A creator’s playground in miniature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. BenQ Monitor Light Bar&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.amazonaws.com%2Fuploads%2Farticles%2Fesxgg5au2821ji4v9h01.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.amazonaws.com%2Fuploads%2Farticles%2Fesxgg5au2821ji4v9h01.png" alt=" " width="676" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Immediate upgrade to any desk setup. Reduces eye strain, looks clean, and helps developers see their code clearly even during late-night “just one more function” sessions without taking extra space on their desk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. 100W GaN Charger&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.amazonaws.com%2Fuploads%2Farticles%2Feznqyferd6siq0nsnwa1.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.amazonaws.com%2Fuploads%2Farticles%2Feznqyferd6siq0nsnwa1.png" alt=" " width="738" height="690"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tiny but absurdly powerful, just like that one script they wrote at 3 AM that still runs in production. A GaN charger keeps everything powered: laptop, phone, tablet, e-reader, existential dread, everything.&lt;/p&gt;




&lt;p&gt;If you love someone who spends their days coaxing data out of websites, obsessing over keyboard switches, or whispering sweet nothings to their terminal, this list has something that will make them light up.&lt;br&gt;
Here’s to a warm, restful holiday season… and to fewer bugs in 2026. Happy gifting, happy coding, and as always, happy scraping! 🕷️✨&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>holiday</category>
      <category>python</category>
      <category>developers</category>
    </item>
    <item>
      <title>Build Your Own Holiday Deal Tracker with Python, Zyte API &amp; IFTTT 🎁</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Fri, 21 Nov 2025 11:06:18 +0000</pubDate>
      <link>https://dev.to/extractdata/build-your-own-holiday-deal-tracker-with-python-zyte-api-ifttt-3p78</link>
      <guid>https://dev.to/extractdata/build-your-own-holiday-deal-tracker-with-python-zyte-api-ifttt-3p78</guid>
      <description>&lt;p&gt;&lt;em&gt;(A simple, developer-friendly project to help you catch Black Friday, Cyber Monday, and Christmas deals before anyone else)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Watch the video tutorial : &lt;/p&gt;

&lt;h2&gt;
  
  
  

  &lt;iframe src="https://www.youtube.com/embed/7o-S-FY5-k8"&gt;
  &lt;/iframe&gt;



&lt;/h2&gt;

&lt;p&gt;It’s that time of the year again, everyone’s hunting for discounts, limited-time deals, and that one item you’ve been keeping an eye on all year. You know the drill: tabs open everywhere, price tracker sites breaking under traffic, browser extensions that promise magic but fail right when The Deal drops.&lt;/p&gt;

&lt;p&gt;So this year, I decided to build something different, something that actually works when I need it the most.&lt;/p&gt;

&lt;p&gt;A personal price-alert system, powered by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 🐍&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.zyte.com/" rel="noopener noreferrer"&gt;Zyte’s&lt;/a&gt; Automatic Extraction API 🕷️&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://ifttt.com/" rel="noopener noreferrer"&gt;IFTTT&lt;/a&gt; mobile notification trigger 📱&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly? It ended up being one of the simplest, useful and most reliable holiday project I’ve made in a long time. &lt;/p&gt;

&lt;p&gt;❌ No HTML parsing.&lt;br&gt;
❌ No complex CSS selectors.&lt;br&gt;
❌ No brittle scrapers that break during peak traffic.&lt;br&gt;
✅Just a clean API call, structured product data, and a push notification when the price hits your set target. &lt;/p&gt;

&lt;p&gt;Let me walk you through the whole thing :&lt;/p&gt;


&lt;h2&gt;
  
  
  🎁 Why Build Your Own Deal Tracker?
&lt;/h2&gt;

&lt;p&gt;Because holiday deals don’t wait for anyone. Every year I get messages from friends asking,&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Bro, what’s the best price tracker? Nothing seems to work today.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And they’re right. Most free services throttle, break, or go down completely during Black Friday and Cyber Monday because everyone hits them at once.&lt;/p&gt;

&lt;p&gt;Meanwhile, with a few lines of Python and Zyte’s AI-powered extraction, you can build something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works only for you&lt;/li&gt;
&lt;li&gt;Deals with anti-ban, anti-bot and captchas &lt;/li&gt;
&lt;li&gt;Checks as often as you want (make it run on your pc, github-actions, server or a raspberry pi)&lt;/li&gt;
&lt;li&gt;Doesn’t rely on brittle selectors&lt;/li&gt;
&lt;li&gt;Doesn’t choke on JavaScript&lt;/li&gt;
&lt;li&gt;Doesn’t get rate-limited&lt;/li&gt;
&lt;li&gt;Sends you a mobile ping instantly when price drops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s the perfect mix of practical dev fun + a tool you’ll actually use.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎄 Why Zyte?
&lt;/h2&gt;

&lt;p&gt;Scraping e-commerce sites for price data is usually a pain: blocking, JavaScript rendering, cookie rules, bot detection, dynamic DOMs, infinite variations in product page layouts... you get it.&lt;/p&gt;

&lt;p&gt;The magic here is &lt;strong&gt;&lt;a href="https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/response/200/product" rel="noopener noreferrer"&gt;Zyte’s Automatic Extraction&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You literally tell AI powered Zyte API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ 
"url": "&amp;lt;product-url&amp;gt;", 
"product": true 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it returns structured fields like: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;price &lt;/li&gt;
&lt;li&gt;currency &lt;/li&gt;
&lt;li&gt;product name &lt;/li&gt;
&lt;li&gt;sku &lt;/li&gt;
&lt;li&gt;images &lt;/li&gt;
&lt;li&gt;stock availability &lt;/li&gt;
&lt;li&gt;description &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t write selectors. You don’t parse HTML. You don’t chase CSS changes. You just get clean data. And that makes this holiday project absurdly simple.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎅 What We’re Building
&lt;/h2&gt;

&lt;p&gt;A script that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes:&lt;/li&gt;
&lt;li&gt;a product URL&lt;/li&gt;
&lt;li&gt;a target price&lt;/li&gt;
&lt;li&gt;your Zyte API key&lt;/li&gt;
&lt;li&gt;&lt;p&gt;your IFTTT Webhook key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fetches structured product data using Zyte API&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checks if the product price is ≤ your target&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If yes → triggers an IFTTT event&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your phone instantly notifies you with product URL:&lt;br&gt;
“Your product dropped to your target price. Go get it!”&lt;/p&gt;
&lt;h2&gt;
  
  
  You can run this:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;manually (when you want)&lt;/li&gt;
&lt;li&gt;on a cron job (in the background)&lt;/li&gt;
&lt;li&gt;in GitHub Actions&lt;/li&gt;
&lt;li&gt;on a Raspberry Pi&lt;/li&gt;
&lt;li&gt;on a cloud function
Your call.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🛠 Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Clone your project
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/apscrapes/zyte-sale-alert.git
cd zyte-sale-alert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Create your virtual environment
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Install dependencies
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Create and add secrets in .env file
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZYTE_API_KEY=your_zyte_key_here (obtained from zyte)
IFTTT_KEY=your_webhooks_key (obtained from IFTTT, see next step)
IFTTT_EVENT=price_drop (name of your ifttt applet, see next step)
TARGET_PRICE=149.99 (example)
PRODUCT_URL=https://example.com/product/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Create an IFTTT Webhook Applet&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;a. Download &lt;a href="https://ifttt.com/" rel="noopener noreferrer"&gt;IFTTT&lt;/a&gt; mobile app, it’s paid but you get a 7-day trial and it has tons of automation you can build using no-code, so i think it’s worth it. &lt;/p&gt;

&lt;p&gt;b. Click create new automation / applet&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.amazonaws.com%2Fuploads%2Farticles%2F7uje1n43d1odgh872t1g.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.amazonaws.com%2Fuploads%2Farticles%2F7uje1n43d1odgh872t1g.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;c. In “IF” field, select “Webhooks” &amp;gt; Add Event Name &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.amazonaws.com%2Fuploads%2Farticles%2F3j8wuf71g071i1wo949c.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.amazonaws.com%2Fuploads%2Farticles%2F3j8wuf71g071i1wo949c.jpg" alt=" "&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Flbtj4o3uiwrv3hq1yzee.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.amazonaws.com%2Fuploads%2Farticles%2Flbtj4o3uiwrv3hq1yzee.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;d. In “THEN” field select Notification &amp;gt; App Notification &amp;gt; JSON Payload &amp;gt; High Priority&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.amazonaws.com%2Fuploads%2Farticles%2Fqkbr84te5l7gsd86oilz.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.amazonaws.com%2Fuploads%2Farticles%2Fqkbr84te5l7gsd86oilz.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note : You can instead of notification can also set other automation like getting an email for example&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.amazonaws.com%2Fuploads%2Farticles%2Flcao83gt5mwlzh1fy0sd.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.amazonaws.com%2Fuploads%2Farticles%2Flcao83gt5mwlzh1fy0sd.jpg" alt=" "&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fyabr1st8f5aia47sggtc.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.amazonaws.com%2Fuploads%2Farticles%2Fyabr1st8f5aia47sggtc.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;e. Here's how it should look like when it's successfully created:&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.amazonaws.com%2Fuploads%2Farticles%2F4bdxpa8pyosqkr39c431.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.amazonaws.com%2Fuploads%2Farticles%2F4bdxpa8pyosqkr39c431.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Once the automation is created add your IFTTT_KEY and IFTTT_EVENT to your .env file &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set product parameters 
The main script is at src/pricedrop.py, edit the following variables to add your:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRODUCT_URL = “ ”
DESIRED_PRICE = 250 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;PRODUCT_URL “ ” is the URL of the product you want to track and DESIRED_PRICE is the price at which you want to be notified, that is it. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the project
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python src/pricedrop.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can run it manually or set up a cronjob to run at regular intervals. Whenever the price drops equal or below your target price you’ll get a notification from the IFTTT app on your phone with product URL so you can order it right away. &lt;/p&gt;

&lt;p&gt;Let’s understand how it works. &lt;/p&gt;

&lt;p&gt;🧩 Core Logic (Short Version)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resp = client.get({"url": url, "product": True})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“product : True” tells zyte API that the webpage we’re scraping has a product so the Machine Learning powered scrapper gets you all the relevant parameters like price, quantity, description, currency etc. &lt;/p&gt;

&lt;p&gt;And that’s literally all you need.&lt;/p&gt;

&lt;p&gt;The reason this works so beautifully is Zyte is handling:&lt;br&gt;
JS rendering&lt;br&gt;
blocking&lt;br&gt;
retries&lt;br&gt;
browser simulation&lt;br&gt;
extraction logic&lt;br&gt;
AI-powered field detection&lt;/p&gt;




&lt;p&gt;In-detail : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Importing all the necessary libraries
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import sys
import requests
from zyte_api import ZyteAPI
from dotenv import load_dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Setting required variables
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRODUCT_URL = "https://outdoor.hylnd7.com/product/a1b2c3d4-e5f6-4a7b-8c9d-000000000293"
DESIRED_PRICE = 250
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we've setup a sample product link whose price we want to track and a price at which if it goes below we want to be notified. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading the API keys from .env file
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load_dotenv()

# from Zyte API
ZYTE_API_KEY = os.getenv("ZYTE_API_KEY")

# from IFTTT Service applets
EVENT_NAME= os.getenv("EVENT_NAME")
IFTTT_KEY= os.getenv("IFTTT_KEY")

if not ZYTE_API_KEY:
    print("ERROR: ZYTE_API_KEY not found in environment.")
    sys.exit(1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the project root directory, create a .env file and add ZYTE API Key you'll get after logging into zyte.com and IFTTT webhook API key you get after creating the automation applet&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Function to trigger the mobile notification :
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def trigger_ifttt(event_name, key, value1):
    url = f"https://maker.ifttt.com/trigger/{event_name}/json/with/key/{key}"

    payload = {
        "value1": value1,
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this funciton is doing is basically making an API call to IFTTT and IFTTT applet is set so whenever the API calls comes with payload it sends mobile notification with that payload, which in this case is product URL so you can directly click and open the product page and buy it before it goes out of stock, SMART right? 😉&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scraping init
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client = ZyteAPI(api_key=ZYTE_API_KEY)

    payload = {
        "url": PRODUCT_URL,
        "product": True,          
    }

    resp = client.get(payload)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making a GET request on Zyte API with product : True, we're asking zyte to treat the URL as product page and thus it's uses it's ML capabilities to fetch product relevant details, price in this case. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare price to SETPOINT
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if price_float &amp;lt;= DESIRED_PRICE:
            trigger_ifttt(EVENT_NAME, IFTTT_KEY, value1 = PRODUCT_URL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If price of the product reaches to or below our target price it will call the IFTTT function, thus triggering the notification.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌟 Make It Even Better
&lt;/h2&gt;

&lt;p&gt;You can extend this to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track multiple URLs&lt;/li&gt;
&lt;li&gt;Log daily prices to CSV&lt;/li&gt;
&lt;li&gt;Plot graphs&lt;/li&gt;
&lt;li&gt;Send WhatsApp alerts&lt;/li&gt;
&lt;li&gt;Push to a Telegram bot&lt;/li&gt;
&lt;li&gt;Use GitHub Actions to check every hour&lt;/li&gt;
&lt;li&gt;Deploy as a Streamlit dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zyte handles the extraction. You build the magic on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧘 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I love projects like this because they hit the sweet spot between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seasonal usefulness &lt;/li&gt;
&lt;li&gt;real-world scraping challenges&lt;/li&gt;
&lt;li&gt;a clean developer experience&lt;/li&gt;
&lt;li&gt;a fun weekend build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're new to the world of web scraping like me, this shows how powerful the right tools can be. If you're experienced, it’s refreshing to skip the boilerplate and let Zyte handle the messy parts.&lt;/p&gt;

&lt;p&gt;And honestly, there’s something fun about getting a custom alert on your phone saying:&lt;br&gt;
“Hey, that gadget you wanted all year just dropped to your target price.”&lt;/p&gt;

&lt;p&gt;Happy building, happy holidays, and happy deal-hunting! 🎄🎁&lt;br&gt;
Let me know what you end up tracking.&lt;/p&gt;

&lt;p&gt;Join Zyte Discord to share what you're building or get any support : &lt;br&gt;
&lt;a href="https://discord.com/invite/DwTnbrm83s" rel="noopener noreferrer"&gt;https://discord.com/invite/DwTnbrm83s&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/iayanpahwa"&gt;@iayanpahwa&lt;/a&gt; &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>python</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Build a crypto miner using Raspberry Pi in 10 minutes</title>
      <dc:creator>Ayan Pahwa</dc:creator>
      <pubDate>Wed, 15 Jun 2022 13:44:17 +0000</pubDate>
      <link>https://dev.to/iayanpahwa/build-a-crypto-miner-using-raspberry-pi-in-10-minutes-3a8g</link>
      <guid>https://dev.to/iayanpahwa/build-a-crypto-miner-using-raspberry-pi-in-10-minutes-3a8g</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fth8kwip1qd7s8ex9n6ef.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.amazonaws.com%2Fuploads%2Farticles%2Fth8kwip1qd7s8ex9n6ef.png" alt="Mine Monero on Raspberry Pi" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lately I’ve been hearing a lot of hype around web3- cryptocurrencies, NFTs, DAOs, DeFi, GameFi and all of these cool jargons giving me a lot of FOMO (Fear of missing out), so I decided that I’m gonna try some of them for myself and see what’s it all about. &lt;/p&gt;

&lt;p&gt;I tried a few things like buying some cryptos on an exchange, made a couple of web3 projects and joined a gazillion of discord channels doing the morning ritual of gm(good morning!) and responding to WAGMI(we all gonna make it!) but this one specific project called &lt;a href="https://www.getmonero.org/" rel="noopener noreferrer"&gt;Monero&lt;/a&gt; caught my attention and I wanted to share it with the community here. &lt;/p&gt;

&lt;p&gt;Before proceeding I want to put out that this is in no way an endorsement of the project or whole blockchain/web3 ecosystem in general, nor it’s a financial, get rich by mining cryptocurrency advice. This is in fact a fun project which you can build along to learn more about web3 and being a developer advocate, I’m also gonna use this project to share about the concept of :&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Using a separate docker images for build-time and run-time within the same Dockerfile, we will see why it’s important and how to achieve that later in this post&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Nevertheless, this project is kind of a fun to build, it allows you to mine cryptocurrency called &lt;strong&gt;XMR&lt;/strong&gt; of Monero blockchain, profitable or not you can no doubt brag about mining cryptocurrency at your home to your friends, so let’s get started 😉&lt;/p&gt;

&lt;p&gt;You can skip the inner details and jump straight to the build part down in the post if you like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Without going too much into the detail, for someone new into the web3 ecosystem - A blockchain is a distributed ledger which keeps records of every transaction happening over it. Just like how a bank keeps the record of who sent money to whom, the amount as well as the date and time, similarly in case of blockchain this immutable information is maintained within distributed blocks connected by a network.&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.amazonaws.com%2Fuploads%2Farticles%2Fxi9glt1cff5zztlyiny7.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.amazonaws.com%2Fuploads%2Farticles%2Fxi9glt1cff5zztlyiny7.png" alt="blockchain" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This validation of all the transactions on a blockchain is done by certain users who lend their compute power in the form of miners or validator nodes. There are also some programs called smart contracts which can run automatically on blockchain when certain conditions are met but that is out of scope for this blog so will not go into more details of smart contracts. &lt;/p&gt;

&lt;p&gt;When you think of Bitcoin or Ethereum miners you might imagine a big server room consisting of massive GPUs or ASICs machines dedicatedly solving complex cryptographic problems sent to them by blockchain and once they solve it they earn rewards in the form of cryptocurrencies The process of successful submission to earn the rewards follows certain consensus mechanism which can vary depending on the type of blockchain &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.amazonaws.com%2Fuploads%2Farticles%2Flh5gjffhn9zrfh35xzc1.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.amazonaws.com%2Fuploads%2Farticles%2Flh5gjffhn9zrfh35xzc1.png" alt="bitcoin miner farm" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example: Bitcoin blockchain works on a consensus mechanism known as Proof of Work (PoW), while Solana works on Proof of Stake (PoS) and Proof of History(PoH). &lt;br&gt;
You can read all about types of consensus mechanism &lt;a href="https://www.allerin.com/blog/8-blockchain-consensus-mechanisms-you-should-know-about" rel="noopener noreferrer"&gt;here&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.amazonaws.com%2Fuploads%2Farticles%2F6gro8hnotu3fizohfnhk.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.amazonaws.com%2Fuploads%2Farticles%2F6gro8hnotu3fizohfnhk.png" alt="consensus mechanisms" width="800" height="1131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re feeling disheartened seeing the cost of these mining computers crushing your dream to have a mining rig of yourself, don’t be because the project we’re talking today called Monero allows or in fact encourages mining on CPUs, so even a small single board computer like Raspberry Pi 4 can become a miner to help validate the transactions and get the rewards in form of XMRs, the cryptocurrency of monero blockchain. &lt;/p&gt;

&lt;p&gt;The philosophy behind this is since the cost and entry barrier to mine is pretty high, the miners are usually owned by few persons or organizations which may not be good for blockchain’s decentralization (no ownership and trustlesness by design), so monero optimizes to allow more and more people to contribute making their blockchain more decentralized. Monero also works on Proof of Work but instead of your small device solving a really complex cryptography puzzle it can join a mining pool with other devices to lend the compute power and together they can solve it fast and depending on how much your device contributed to the solution it’ll be rewarded suitably for that.&lt;/p&gt;

&lt;p&gt;Is it profitable? Maybe not, given the device needs to be powered on and running but it surely is fun and maybe you can really earn if you add a lot of devices in your mining pool with good resources, say Rpi with 8 GB RAM; nevertheless it’s not financial advice so please do your due diligence :) &lt;/p&gt;

&lt;p&gt;You can read all about Monero project, how much it pays for mining, costs associated, etc all on official website: &lt;a href="https://www.getmonero.org/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Build
&lt;/h2&gt;


&lt;h3&gt;
  
  
  Hardware Needed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A single board computer like raspberry pi 4 (more ram the better). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following SBCs were tested by my friend Lambros and this was the hashrate result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Raspberry Pi 3  - 20 H/s
- Raspberry Pi 4, 1 GB RAM - 45 H/s
- Raspberry Pi 4, 4 GB RAM - 99 H/s
- Nvidia Jetson Nano 2GB (without GPU enabled) - 62 H/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hash rate(H/s) is the number of hashes a device can solve per second. I’ve not enabled CUDA(GPU backend) for Nvidia Jetson since monero encourages mining on CPU though if you want to give it a shot I’d love to see how it performs.&lt;/p&gt;

&lt;p&gt;Other things you'll need are&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SD card&lt;/li&gt;
&lt;li&gt;Power supply&lt;/li&gt;
&lt;li&gt;Connectivity to Internet via LAN or WiFi &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Software needed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;monero wallet (this is where you’ll get the rewards) download from &lt;a href="https://www.getmonero.org/downloads/" rel="noopener noreferrer"&gt;https://www.getmonero.org/downloads/&lt;/a&gt;&lt;br&gt;
Information of mining pools : as mentioned earlier we will be joining a mining pool, the address of active mining pools can be found by a simple google query, it’s better to use one near you ex: google search monero mining pools in Los Angeles. Each mining pool has their own threshold after which they start paying out to the wallets. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://balena.io/cloud/" rel="noopener noreferrer"&gt;balenaCloud account&lt;/a&gt; to manage miners&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.balena.io/etcher/" rel="noopener noreferrer"&gt;balenaEtcher&lt;/a&gt; to flash the SD card&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The easy way (deploy with balena)
&lt;/h3&gt;

&lt;p&gt;Sign up for a free balenaCloud account. Your first ten devices are free and fully-featured! Then use the button below to create and deploy the application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/iayanpahwa/monero-miner" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbalena.io%2Fdeploy.svg" alt="deploy button" width="191" height="38"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: I have used a Raspberry Pi 4 in the image below but be sure to select the correct device type for the device you are using.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Select the Device&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add OS type: &lt;a href="https://www.balena.io/docs/reference/OS/overview/#development-vs-production-mode" rel="noopener noreferrer"&gt;Production vs Development&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(optionally) add in your home WiFi credentials, download and flash the OS to SD card using etcher&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Advanced way
&lt;/h3&gt;

&lt;p&gt;If you are already a balena user it might be better for you to use this way. You can clone the project from &lt;a href="https://github.com/iayanpahwa/monero-miner" rel="noopener noreferrer"&gt;this github repo&lt;/a&gt; and use the &lt;a href="https://github.com/balena-io/balena-cli" rel="noopener noreferrer"&gt;balena CLI&lt;/a&gt; command&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;balena push &amp;lt;fleet_name&amp;gt;&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
 to push the application to your devices in the fleet created on dashboard. This is the best option if you want to tinker with the project and have full control. The &lt;a href="https://www.balena.io/os/docs/raspberrypi3/getting-started/" rel="noopener noreferrer"&gt;Getting Started Guide&lt;/a&gt; covers this option. After you've created the application and pushed the code using the CLI, follow the steps below. &lt;/p&gt;
&lt;h4&gt;
  
  
  First device boot and configurations
&lt;/h4&gt;

&lt;p&gt;When the device boots for the first time, it connects to the balenaCloud dashboard, after which you’ll be able to see it listed online. In the meanwhile we need to Install Monero wallet on our computer where we will be getting the mining rewards &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.amazonaws.com%2Fuploads%2Farticles%2Fdy8osmut0h3kxffqfnhr.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fdy8osmut0h3kxffqfnhr.gif" alt="boot the pi" width="500" height="281"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Software Setup
&lt;/h4&gt;

&lt;p&gt;Download the Monero wallet from &lt;a href="https://www.getmonero.org/downloads/" rel="noopener noreferrer"&gt;https://www.getmonero.org/downloads/&lt;/a&gt; GUI or CLI as per your preference. I recommend using a GUI wallet and create a new wallet using Simple Mode.&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.amazonaws.com%2Fuploads%2Farticles%2Fwpvbeaqhobwokxqswc59.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.amazonaws.com%2Fuploads%2Farticles%2Fwpvbeaqhobwokxqswc59.png" alt=" " width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the wallet is created you’ll have a unique wallet address copy that to clipboard and head over to:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;balena cloud &amp;gt; your device &amp;gt; Device Variables&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
tab and add the following &lt;/p&gt;
&lt;h3&gt;
  
  
  Device Variables
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VARIABLE NAME&lt;/th&gt;
&lt;th&gt;VALUE&lt;/th&gt;
&lt;th&gt;CHANGE TYPE&lt;/th&gt;
&lt;th&gt;DESCRIPTION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WALLET_ADDRESS&lt;/td&gt;
&lt;td&gt; from last step&lt;/td&gt;
&lt;td&gt;Must add&lt;/td&gt;
&lt;td&gt;This is the wallet address where you’ll earn your mining rewards in the form of XMRs which you can trade on any crypto exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MINER_POOL&lt;/td&gt;
&lt;td&gt;Default value is:  &lt;a href="http://xmr.2miners.com:2222/" rel="noopener noreferrer"&gt;http://xmr.2miners.com:2222/&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;This is the miner pool you will join by default. You can change this to another miner pool by searching addresses of miner pools on Google. The one near to your location will be good. For ex:&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This will restart the container and your miner will be registered to the mining pool and start getting the jobs. All the rewards will be sent to your monero wallet after your device meets the threshold of the miner pool. &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.amazonaws.com%2Fuploads%2Farticles%2F8qejtzog7a5yc1m523uj.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.amazonaws.com%2Fuploads%2Farticles%2F8qejtzog7a5yc1m523uj.png" alt="Monero Mining logs on balenaCloud" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now as I mentioned above this may not be profitable at all but it's definitely fun and let’s look at some totally unrelated things that we're gonna learn here.&lt;/p&gt;
&lt;h4&gt;
  
  
  Container Learnings
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/iayanpahwa/monero-miner/blob/main/Dockerfile.template" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the project’s Dockerfile.template. If you take a closer look at it, you’ll see there are two different base images being used. One of them is a build image and other one is the run image &lt;/p&gt;

&lt;p&gt;Why we do that is simply because we don't want our run image to be bloated with extra packages which were needed to build a source code. So we conduct the build in a separate image and copy the artifacts or binaries in the run image using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COPY --from=build /usr/src/app/xmrig/build/xmrig /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way our main image is minimal and less bloated. All of the balena base images are available as build and run. The build image has additional packages such as &lt;em&gt;gcc&lt;/em&gt;, &lt;em&gt;build-essential&lt;/em&gt; which are needed to build from source whereas the run image is minimal. You can read more about it &lt;a href="https://www.balena.io/docs/reference/base-images/base-images/#run-vs-build" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;So this was the container lesson from this blog. Now I hope you do use this project to dip into the world where IoT edge computing meets web3 and as always if you have any suggestions, feedbacks or questions, please write on &lt;a href="http://forums.balena.io" rel="noopener noreferrer"&gt;balenaForums&lt;/a&gt; or any social media channel. I'll be back with another interesting project and lesson soon 🍻&lt;/p&gt;




&lt;p&gt;Attribution&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xmrig" rel="noopener noreferrer"&gt;XMRig project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;===&lt;/p&gt;

</description>
      <category>web3</category>
      <category>tutorial</category>
      <category>blockchain</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
