<?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: WAFER</title>
    <description>The latest articles on DEV Community by WAFER (@chex0210crypto).</description>
    <link>https://dev.to/chex0210crypto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4002435%2Fa809d1e4-842f-4a7c-a280-56c3cfb3650c.jpg</url>
      <title>DEV Community: WAFER</title>
      <link>https://dev.to/chex0210crypto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chex0210crypto"/>
    <language>en</language>
    <item>
      <title>WAFER Deep Crawler: 8-Layer Stealth Architecture - Fingerprint Layer</title>
      <dc:creator>WAFER</dc:creator>
      <pubDate>Sat, 27 Jun 2026 13:18:53 +0000</pubDate>
      <link>https://dev.to/chex0210crypto/wafer-deep-crawler-8-layer-stealth-architecture-fingerprint-layer-5bjg</link>
      <guid>https://dev.to/chex0210crypto/wafer-deep-crawler-8-layer-stealth-architecture-fingerprint-layer-5bjg</guid>
      <description>&lt;h1&gt;
  
  
  WAFER Deep Crawler: The 8-Layer Stealth Architecture - Fingerprint Layer Deep Dive
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Part of the WAFER Deep Crawler Series&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Suitable for developers with basic crawling experience who want to bypass advanced anti-bot systems like Cloudflare and Akamai&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Do 90% of Crawlers Get Blocked Immediately?
&lt;/h2&gt;

&lt;p&gt;Most crawlers get blocked not because they request too fast, but because &lt;strong&gt;their fingerprints are too fake&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Modern WAFs collect hundreds of browser characteristics. If just 3-5 don't match real browser patterns, the request is instantly flagged as a bot and returns a 403 or 5-second challenge wall.&lt;/p&gt;

&lt;p&gt;Common rookie mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;requests&lt;/code&gt; directly — your TLS fingerprint screams "bot" from the first handshake&lt;/li&gt;
&lt;li&gt;Default Selenium config — &lt;code&gt;navigator.webdriver = true&lt;/code&gt; is an instant giveaway&lt;/li&gt;
&lt;li&gt;Hardcoded UA says "Chrome 120" but Canvas fingerprint corresponds to Chrome 110 — &lt;strong&gt;feature mismatch&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  WAFER's Three-Layer Fingerprint Defense
&lt;/h2&gt;

&lt;p&gt;Our architecture splits fingerprinting into &lt;strong&gt;HTTP Layer → Browser Layer → TLS Layer&lt;/strong&gt;, each independently controllable. Combined, they simulate 99% of real devices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────┐
│       Behavior Layer (mouse/kb)  │
├─────────────────────────────────┤
│    CDP Browser Fingerprint (19)  │  ← This chapter's focus
├─────────────────────────────────┤
│       TLS Fingerprint (B+D)     │
├─────────────────────────────────┤
│       HTTP Protocol Headers     │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Complete Browser Fingerprint Breakdown (19 Items)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Basic Navigator Properties
&lt;/h3&gt;

&lt;p&gt;The most basic checks: &lt;code&gt;userAgent&lt;/code&gt;, &lt;code&gt;platform&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;plugins&lt;/code&gt;. Every single one must match a real browser profile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fingerprint&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;user_agent&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;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36&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;platform&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;Win32&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;languages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&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;en&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;plugins_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Real Chrome has exactly 5
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webdriver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;   &lt;span class="c1"&gt;# Critical: must be false
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Canvas &amp;amp; WebGL Fingerprinting
&lt;/h3&gt;

&lt;p&gt;This is the most common failure point: automation tools render Canvas differently from real browsers — down to individual pixels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Don't randomize pixels (consistency checks will catch you). Instead, use a curated profile library that maps to real device fingerprints.&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;override_webgl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vendor&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;Google Inc. (NVIDIA)&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;renderer&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;ANGLE (NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0 D3D11)&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;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_init_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        const origGetParameter = WebGLRenderingContext.prototype.getParameter;
        WebGLRenderingContext.prototype.getParameter = function(p) {{
            if (p === 37445) return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vendor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;
            if (p === 37446) return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;renderer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;
            return origGetParameter.call(this, p);
        }};
    &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;
  
  
  3. Other Critical Checks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio fingerprint&lt;/strong&gt;: Sample rate, channel count must match the UA's platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font list&lt;/strong&gt;: Windows has 200+ default fonts; don't miss any&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timezone/Geo&lt;/strong&gt;: IP location must match timezone, or you're flagged as proxy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware concurrency&lt;/strong&gt;: &lt;code&gt;navigator.hardwareConcurrency&lt;/code&gt; should match real CPU cores, not a hardcoded 4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen resolution&lt;/strong&gt;: Must be a common resolution (1920x1080, 1440x900, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch support&lt;/strong&gt;: Mobile UA must have &lt;code&gt;ontouchstart&lt;/code&gt; defined; desktop UA must not&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device memory&lt;/strong&gt;: &lt;code&gt;navigator.deviceMemory&lt;/code&gt; should be realistic (4, 8, or 16 GB)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Headless Detection Vectors
&lt;/h3&gt;

&lt;p&gt;Modern WAFs check dozens of headless indicators:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Real Browser&lt;/th&gt;
&lt;th&gt;Headless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;navigator.webdriver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chrome.runtime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;exists&lt;/td&gt;
&lt;td&gt;missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window.chrome&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;defined&lt;/td&gt;
&lt;td&gt;undefined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;navigator.plugins.length&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;≥ 5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;navigator.languages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["en-US", "en"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["en-US"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;document.hidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tracks visibility&lt;/td&gt;
&lt;td&gt;always false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebGL vendor&lt;/td&gt;
&lt;td&gt;GPU name&lt;/td&gt;
&lt;td&gt;"Google SwiftShader"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  TLS Fingerprint: B vs D Strategy
&lt;/h2&gt;

&lt;p&gt;JA3 fingerprinting is the hardest HTTP-layer check. Vanilla &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;aiohttp&lt;/code&gt; JA3 hashes are all on blocklists.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;curl-impersonate mimicking Chrome's native TLS stack&lt;/td&gt;
&lt;td&gt;High-concurrency API scraping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real browser TLS stack via CDP&lt;/td&gt;
&lt;td&gt;High-difficulty targets, 5-second challenge pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;WAFER auto-degrades&lt;/strong&gt;: Start with B, fall back to D on failure. Balances speed and pass rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fingerprint Consistency Principle (90% of People Get This Wrong)
&lt;/h2&gt;

&lt;p&gt;All layers must be &lt;strong&gt;self-consistent&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UA is Chrome 125 → Canvas fingerprint must match Chrome 125 (not Chrome 120's)&lt;/li&gt;
&lt;li&gt;IP is in Beijing → timezone must be GMT+8, language must include &lt;code&gt;zh-CN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Platform is macOS → scroll speed must be the macOS inertial rate (not Windows 3-line tick)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One mismatch drops your bot score by 30 points. Guaranteed block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Exercise
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Test your crawler's fingerprint with &lt;a href="https://abrahamjuliot.github.io/creepjs/" rel="noopener noreferrer"&gt;CreepJS&lt;/a&gt; — what's your score?&lt;/li&gt;
&lt;li&gt;Compare your crawler's Canvas fingerprint with a real Chrome — find 3 differences&lt;/li&gt;
&lt;li&gt;Modify one fingerprint parameter and observe the change in your target site's block rate&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Next Chapter: Turnstile CAPTCHA Full-Chain Auto-Solving — from sitekey extraction to Cloudflare siteverify validation&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>tutorial</category>
      <category>selenium</category>
    </item>
    <item>
      <title>LangGraph's Routing Is LLM-Guessing. I Wrote 50 Lines of Code to Make It Deterministic.</title>
      <dc:creator>WAFER</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:18:34 +0000</pubDate>
      <link>https://dev.to/chex0210crypto/langgraphs-routing-is-llm-guessing-i-wrote-50-lines-of-code-to-make-it-deterministic-24oj</link>
      <guid>https://dev.to/chex0210crypto/langgraphs-routing-is-llm-guessing-i-wrote-50-lines-of-code-to-make-it-deterministic-24oj</guid>
      <description>&lt;p&gt;Every time your LangGraph agent sees "check nginx logs", it might call a different tool.&lt;/p&gt;

&lt;p&gt;That's not an exaggeration. LangGraph's routing is driven by an LLM prompt — and prompts aren't reproducible. Same input, different day, different LLM mood, different tool selected.&lt;/p&gt;

&lt;p&gt;I spent months debugging this. Every wrong route meant tweaking a prompt, hoping the next call would be better. It never was.&lt;/p&gt;

&lt;p&gt;So I wrote something different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Routing by LLM is fragile
&lt;/h2&gt;

&lt;p&gt;LangGraph is great at orchestrating multi-step agent workflows. But the first step — "what tool should I use?" — is a black box.&lt;/p&gt;

&lt;p&gt;You define a prompt, the LLM decides. If it chooses wrong, you can't debug it. You can only guess: was the prompt not specific enough? Too specific? Wrong example?&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;routing problem&lt;/strong&gt;: given user input, which domain or tool should handle it? LangGraph leaves this to the LLM. I think it shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: 50 Lines of YAML + Python
&lt;/h2&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;decide_router&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RouteTable&lt;/span&gt;

&lt;span class="n"&gt;rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RouteTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routes.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check nginx error logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# rule.domain → "monitoring" — always, every time
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No LLM call. No prompt. Just a YAML file and 50 lines of matching logic.&lt;/p&gt;

&lt;p&gt;The YAML defines domains with keywords and regex patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;domains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;monitoring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;monitor&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;patterns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(check|look).*(log|status|error)"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;coding&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;write&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;human&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;delete production&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;restart cluster&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;require_confirm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plug it into LangGraph as a node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Graph&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decide_router&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RouteTable&lt;/span&gt;

&lt;span class="n"&gt;rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RouteTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routes.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;routing_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decide_route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routing_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decide_route&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;Your LangGraph agent now routes with &lt;strong&gt;rules&lt;/strong&gt;, not guesses. Same input → same domain. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Surprising Part: It Learns From Corrections
&lt;/h2&gt;

&lt;p&gt;The best part wasn't planned — it emerged from using it.&lt;/p&gt;

&lt;p&gt;I noticed I kept correcting wrong routes. "That's monitoring, not coding." Every correction was a signal. So I added a feedback cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# User corrects once → remembers immediately
&lt;/span&gt;&lt;span class="n"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check nginx errors&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;monitoring&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;Same correction 3 times → becomes a permanent routing rule at priority 70. Rules not used in 30 days auto-delete. The system gets smarter with every correction. No fine-tuning. No prompt engineering.&lt;/p&gt;

&lt;p&gt;The core module is one file, 200 lines of Python. Read the whole thing in 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&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;decide-router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or browse the code: &lt;a href="https://github.com/chex0210-crypto/decide-router" rel="noopener noreferrer"&gt;github.com/chex0210-crypto/decide-router&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I built this because I was tired of guessing why my agent picked the wrong tool. If you've had the same frustration, star the repo — it helps other people find it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>langgraph</category>
      <category>agents</category>
      <category>python</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
