<?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: Leon</title>
    <description>The latest articles on DEV Community by Leon (@leonting1010).</description>
    <link>https://dev.to/leonting1010</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%2F3851369%2F7a1d458f-c463-4b40-8bb1-69e5711e435d.png</url>
      <title>DEV Community: Leon</title>
      <link>https://dev.to/leonting1010</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leonting1010"/>
    <language>en</language>
    <item>
      <title>Playwright MCP burns 114k tokens for one workflow. Here's why, and what to do about it.</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Wed, 22 Apr 2026 04:50:04 +0000</pubDate>
      <link>https://dev.to/leonting1010/playwright-mcp-burns-114k-tokens-for-one-workflow-heres-why-and-what-to-do-about-it-57k8</link>
      <guid>https://dev.to/leonting1010/playwright-mcp-burns-114k-tokens-for-one-workflow-heres-why-and-what-to-do-about-it-57k8</guid>
      <description>&lt;p&gt;A recent r/ClaudeAI post measured a single Playwright MCP workflow at &lt;strong&gt;114,000 tokens&lt;/strong&gt;. Not a complex task — a 7-step navigation + form submission that ran in under a minute. Same workflow as a compiled &lt;code&gt;tap.run&lt;/code&gt;: zero tokens.&lt;/p&gt;

&lt;p&gt;This isn't "Playwright MCP is bad." It's a structural property of running an LLM at runtime versus compile time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tokens go
&lt;/h2&gt;

&lt;p&gt;Each Playwright MCP call sends back to the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The current page's accessibility tree (~5-15K tokens for a typical SPA)&lt;/li&gt;
&lt;li&gt;A screenshot encoded as base64 (~2-8K tokens depending on quality)&lt;/li&gt;
&lt;li&gt;The console output since last call&lt;/li&gt;
&lt;li&gt;The action result + any error context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model needs all of that to decide the next action. So a 7-step workflow = 7 × (~15K) = ~100K tokens. Add the schema injection at session start (~1.3K per tool, ~28 tools loaded eagerly = ~36K) and you're at the 114K observed.&lt;/p&gt;

&lt;p&gt;The optimisations help — DOM compression, accessibility-only modes, smaller screenshots — but the per-step cost is still proportional to page complexity. Add interaction depth and the cost goes up linearly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compiler alternative
&lt;/h2&gt;

&lt;p&gt;The insight &lt;code&gt;tap forge&lt;/code&gt; is built on: most browser automation is a known workflow. You're not exploring; you're executing the same task on the same site, repeatedly. The LLM is needed to &lt;strong&gt;figure out how&lt;/strong&gt; the first time. After that, it's overhead.&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="c"&gt;# First time — LLM authors the program&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap forge https://example.com/login → submit
✓ Inspected: form#login, 3 fields
✓ Verified: redirect to /dashboard, status 200
✓ Saved: example/login.tap.js   &lt;span class="o"&gt;(&lt;/span&gt;47 lines of JavaScript&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Forever after — no LLM, no tokens&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap example login &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alice &lt;span class="nv"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx   &lt;span class="c"&gt;# 200ms, $0.00&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap example login &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alice &lt;span class="nv"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx   &lt;span class="c"&gt;# 200ms, $0.00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the workflow that cost 114K tokens with Playwright MCP, the equivalent &lt;code&gt;.tap.js&lt;/code&gt; file is ~80 lines. It runs in 200ms. Token cost: &lt;strong&gt;0&lt;/strong&gt; (after the one-time forge).&lt;/p&gt;

&lt;h2&gt;
  
  
  When each makes sense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Playwright MCP wins when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workflow is unique each time (agent exploration)&lt;/li&gt;
&lt;li&gt;The site changes structure between runs (no stable program possible)&lt;/li&gt;
&lt;li&gt;You're prototyping and don't yet know what you want to extract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Compiled taps win when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You run the same workflow more than ~5 times&lt;/li&gt;
&lt;li&gt;The site's structural pattern is stable (~95% of sites — most A/B tests don't change DOM, just CSS)&lt;/li&gt;
&lt;li&gt;You need monitoring (deterministic output = row count is a signal)&lt;/li&gt;
&lt;li&gt;You need offline execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The break-even is low. Even at $5/MTok, one Playwright MCP run of 100K tokens = $0.50. Five runs = $2.50, more than the entire $9/mo Hacker tier of a compiler-based tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two structural differences worth understanding
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Output consistency.&lt;/strong&gt; When the same site/same prompt produces slightly different extractions across runs (the LLM is non-deterministic), monitoring is structurally hard. Row count fluctuation isn't noise — it's the model. With a compiled tap, row count fluctuation IS noise, and you can alert on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Failure detection.&lt;/strong&gt; Playwright MCP detects failure reactively — the tool call returns an error, the LLM sees it, retries with a different approach. By the time you notice, tokens are spent and time is lost. Compiled taps detect failure proactively via fingerprint diffing — &lt;code&gt;tap doctor&lt;/code&gt; checks if the page structure changed BEFORE the run fires. If drifted, the run doesn't even start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark question
&lt;/h2&gt;

&lt;p&gt;Honest comparison: Playwright MCP has been the most flexible browser-agent setup for the past year. The 114K tokens is the price you pay for that flexibility. If your workflow is varied enough that you need it, pay it. If your workflow is the same automation run 1,000 times, paying it is leaving money on the table.&lt;/p&gt;

&lt;p&gt;The broader pattern: every browser-agent tool faces this LLM-at-runtime vs LLM-at-compile-time tradeoff. The question isn't "which tool is better" — it's "does my workload repeat enough to amortize a one-time compile?"&lt;/p&gt;

&lt;p&gt;For most production scrapers, the answer is yes.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>mcp</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>rtrvr.ai vs Taprun: cheaper LLM-at-runtime still isn't zero tokens</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Wed, 22 Apr 2026 04:40:03 +0000</pubDate>
      <link>https://dev.to/leonting1010/rtrvrai-vs-taprun-cheaper-llm-at-runtime-still-isnt-zero-tokens-1ljf</link>
      <guid>https://dev.to/leonting1010/rtrvrai-vs-taprun-cheaper-llm-at-runtime-still-isnt-zero-tokens-1ljf</guid>
      <description>&lt;p&gt;&lt;a href="https://www.rtrvr.ai/" rel="noopener noreferrer"&gt;rtrvr.ai&lt;/a&gt; is a polished entrant in the browser-agent space. Their architecture is genuinely interesting — "DOM-native" processing with "Smart DOM Compression", 25× cheaper than vision-based alternatives, 81% SOTA accuracy on their reported benchmark. They ship Chrome extension, Cloud dashboard, API, MCP server, CLI, and even a WhatsApp bot. Their landing page lists 10 named competitors. The pricing mirrors Taprun almost exactly — $9.99 / $29.99 / $99.99 / $499.99 per month.&lt;/p&gt;

&lt;p&gt;So when I first read their docs, the obvious question was: do they do what Taprun does? Because if they do, Taprun is in trouble.&lt;/p&gt;

&lt;p&gt;They don't. And the reason sits on a single architectural line every browser-agent tool has to pick a side of.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line: is the LLM at authoring, or at runtime?
&lt;/h2&gt;

&lt;p&gt;There are two fundamentally different ways to point an LLM at a browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LLM at runtime.&lt;/strong&gt; The model is called every time the automation executes. Each run is an inference pass. Optimisations make that pass smaller — DOM compression, accessibility trees instead of screenshots, smaller models — but the pass is still there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LLM at authoring.&lt;/strong&gt; The model is called once, during setup. It reads the site, figures out the structure, and emits a deterministic program. From then on, the program runs. The LLM never gets called again.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser Use, Stagehand, Playwright MCP, and rtrvr.ai all sit on side (1). They differ in &lt;em&gt;how&lt;/em&gt; they call the LLM — vision vs DOM, big model vs small model, whole page vs compressed — but not in &lt;em&gt;whether&lt;/em&gt; they call it.&lt;/p&gt;

&lt;p&gt;Taprun sits on side (2). &lt;code&gt;tap forge&lt;/code&gt; runs the LLM once to author a &lt;code&gt;.tap.js&lt;/code&gt; file. &lt;code&gt;tap.run&lt;/code&gt; executes that file forever with zero inference.&lt;/p&gt;

&lt;p&gt;This distinction isn't marketing. It's Python vs compiled C. Both evaluate expressions; one evaluates at runtime, the other at compile time. You pick based on whether the workload repeats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where rtrvr.ai is genuinely strong
&lt;/h2&gt;

&lt;p&gt;Credit where it's due. rtrvr gets a lot right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Form-factor breadth.&lt;/strong&gt; Chrome extension, Cloud dashboard, Sheets integration, REST API, MCP server, CLI, embeddable widget, WhatsApp bot. Each one hits a different user at a different moment. This is good distribution design.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"No CDP = No Failures" framing.&lt;/strong&gt; Chrome DevTools Protocol automation is bot-detectable; their architecture avoids it. This is a real reliability argument.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;25× cheaper vs vision.&lt;/strong&gt; Replacing screenshots (~114K tokens) with accessibility trees and DOM compression (~26K tokens) is a meaningful improvement. Their claimed $0.12 per task is genuine progress.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;BYOK path.&lt;/strong&gt; "Bring your own key or local endpoint" collapses their cost to roughly your LLM provider's invoice. Clever.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit competitive comparison.&lt;/strong&gt; They list 10 competitors by name on their landing page. It's confident. It invites comparison. That's a good sign.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your use case is agentic exploration — new sites, unknown tasks, one-off interactions — rtrvr is a serious tool. I'd reach for it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the LLM-at-runtime model hits a ceiling
&lt;/h2&gt;

&lt;p&gt;The ceiling isn't quality. It's structural.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-run cost scales linearly with runs.&lt;/strong&gt; 26K tokens per task × 1,000 runs/day = 26M tokens/day. At Gemini Flash Lite rates that's real money; at Gemini Pro rates it's ~$260/day. rtrvr's own pricing acknowledges this: the Basic tier is 1,500 credits/month, which at "5 credits/task" is ~300 tasks. A single production workflow running every 5 minutes eats that budget in three days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output variance is by design.&lt;/strong&gt; When the same page, same prompt, same task produces slightly different extractions across runs, you can't build monitoring around it. Row count fluctuation isn't "a bug" when the system is designed to re-interpret the page every time. The 81% SOTA accuracy number is a fine benchmark result, but it means 19% of invocations are wrong in some way, and you don't know which 19%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Self-healing" still pays tokens to heal.&lt;/strong&gt; Every browser-agent tool in this category markets "self-healing". What they mean is: when the selector breaks, the LLM re-runs to figure out the new one. That's real, and it's useful — but it is &lt;em&gt;reactive&lt;/em&gt;. The task has already failed (or silently returned garbage) before healing kicks in, and every heal is another inference pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Taprun does differently — structurally, not just cheaper
&lt;/h2&gt;

&lt;p&gt;Taprun moves the LLM to authoring time. Once.&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="c"&gt;# Authoring: LLM inspects once, emits deterministic code&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap forge https://reddit.com/r/programming
✓ Inspected: REST API detected at oauth.reddit.com
✓ Verified: 25 rows, score 95/100
✓ Saved: reddit/hot.tap.js  &lt;span class="o"&gt;(&lt;/span&gt;pure JavaScript, on your disk&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Runtime: no LLM, no tokens, same output every time&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap reddit hot     &lt;span class="c"&gt;# 25 rows, ~200 ms, $0.00&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap reddit hot     &lt;span class="c"&gt;# 25 rows, ~200 ms, $0.00&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap reddit hot     &lt;span class="c"&gt;# 25 rows, ~200 ms, $0.00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the output is deterministic, monitoring is tractable. Because execution is deterministic, row count &lt;em&gt;is&lt;/em&gt; a health signal. Because the program is on your disk, it works offline and doesn't depend on anyone's cloud.&lt;/p&gt;

&lt;p&gt;And the "self-healing" axis flips from reactive to proactive:&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="nv"&gt;$ &lt;/span&gt;tap doctor &lt;span class="nt"&gt;--auto&lt;/span&gt; reddit hot
✗ selector div.thing — gone since last run
⚠ fingerprint diff: ↑ 2 structural changes
✓ heal bundle ready — current code + git &lt;span class="nb"&gt;history&lt;/span&gt; + page snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tap doctor&lt;/code&gt; checks a structural fingerprint &lt;em&gt;before&lt;/em&gt; the run fires. If the site drifted, the run doesn't even start — you get a diff of what changed and a bundle your AI agent can patch offline. No retry tokens. No silent bad data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the numbers actually land
&lt;/h2&gt;

&lt;p&gt;Take a workflow that runs every 5 minutes — 288 runs/day, ~8,640 runs/month. Not extreme; this is a single production scraper.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser Use:&lt;/strong&gt; 8,640 × $0.50 = $4,320/month (lower bound)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rtrvr.ai Basic&lt;/strong&gt; ($9.99): 1,500 credits / 5 per task = 300 tasks. You're over budget by day two. Need Scale ($499.99) — 60,000 credits covers ~12,000 tasks. BYOK path lowers the number but your Gemini bill replaces it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Taprun Free:&lt;/strong&gt; 8,640 runs × $0 = &lt;strong&gt;$0/month&lt;/strong&gt;. You keep the $9 if you want AI to forge new taps for you; you keep the $29 if you want auto-repair on cron.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 10 runs a day, none of this matters. At 10 runs a minute, it's the only thing that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to pick each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick rtrvr.ai when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're doing agentic exploration — new sites, undefined tasks, high variance in what you're extracting&lt;/li&gt;
&lt;li&gt;You want a polished cloud dashboard and don't mind hosted state&lt;/li&gt;
&lt;li&gt;You need WhatsApp or embeddable widget form factors&lt;/li&gt;
&lt;li&gt;Your per-task count stays under ~300/month, or you're comfortable with a $499/mo scale tier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick Taprun when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You run the same automation more than once — and want to know the output is the same every time&lt;/li&gt;
&lt;li&gt;You want the program on your disk, version-controlled, not saved in someone's dashboard&lt;/li&gt;
&lt;li&gt;You want structural fingerprint diffs, not retry loops, as your breakage story&lt;/li&gt;
&lt;li&gt;Your scale is "every 5 minutes forever" and you want the bill to stay $9/mo&lt;/li&gt;
&lt;li&gt;You want to keep working offline and in sandboxed environments where outbound LLM calls aren't allowed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're not really competitors — they're different tools for different moments. Use rtrvr to figure out &lt;em&gt;what&lt;/em&gt; you want to extract. Use Taprun once you know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line summary
&lt;/h2&gt;

&lt;p&gt;rtrvr made LLM-at-runtime 25× cheaper than the vision-based baseline. Taprun made it zero. Those aren't points on the same line.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>browser</category>
      <category>comparison</category>
    </item>
    <item>
      <title>MCP is the authoring layer. Execution should cost zero tokens.</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Wed, 22 Apr 2026 04:15:26 +0000</pubDate>
      <link>https://dev.to/leonting1010/mcp-is-the-authoring-layer-execution-should-cost-zero-tokens-4gdf</link>
      <guid>https://dev.to/leonting1010/mcp-is-the-authoring-layer-execution-should-cost-zero-tokens-4gdf</guid>
      <description>&lt;p&gt;Two posts on Reddit this month independently measured MCP's token overhead. Both reached the same number: &lt;strong&gt;30–40% more tokens than the CLI equivalent.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I added Notion, Sentry and Shortcut MCPs and was surprised to see every session starting off with 40% of the context used."&lt;br&gt;
— NoSlicedMushrooms (28 upvotes), r/ClaudeAI&lt;/p&gt;

&lt;p&gt;"A batch job with 4 MCP servers blew through our token budget in 2 hours. The schema injection on every turn is the killer."&lt;br&gt;
— tom_mathews, r/ClaudeAI&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The "MCP is dead, just use CLI" take followed immediately. But three independent users — in three different threads, on three different subreddits — arrived at the same conclusion: &lt;strong&gt;the problem isn't MCP. It's using MCP for the wrong job.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"MCP for the main orchestrator, CLI for sub-agents. Both hit the same backend."&lt;br&gt;
— raphasouthall, r/mcp (48 upvotes)&lt;/p&gt;

&lt;p&gt;"MCP makes sense for discovery, not for known workflows."&lt;br&gt;
— tom_mathews, r/ClaudeAI&lt;/p&gt;

&lt;p&gt;"Development Tool versus Production Tool. MCP the shit you serve to clients and CLI while building."&lt;br&gt;
— mat8675, r/ClaudeAI&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They're all describing the same architecture. And it's the architecture Tap has used from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two-Layer Model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1: MCP (Authoring)
forge.inspect    → AI analyzes the site
forge.verify     → AI tests the program
forge.save       → program saved to disk

AI participates. Tokens consumed. One-time cost.

─────────────────────────────────────────────

Layer 2: CLI (Execution)
tap.run          → program executes

Zero AI. Zero tokens. Deterministic. Forever.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP is the authoring layer. It's where AI discovers what the site looks like, what API endpoints are available, which selectors match the data, and how to structure the extraction. This is a one-time process — forge — that produces a &lt;code&gt;.tap.js&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;After that, &lt;code&gt;tap.run&lt;/code&gt; executes the program directly. No MCP. No schema injection. No token overhead. The program is JavaScript. It runs in less than a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;raphasouthall measured MCP overhead precisely for a 21-tool server:&lt;/p&gt;

&lt;p&gt;MCPCLI / tap.run&lt;br&gt;
Upfront cost~1,300 tokens (schema injection)0&lt;br&gt;
Per-call cost~800 tokens~750 tokens&lt;br&gt;
After 10 calls~880 tokens/call (amortized)750 tokens/call&lt;/p&gt;

&lt;p&gt;For a single forge session (one-time), ~1,300 tokens of overhead is nothing. For 1,000 daily executions? It's the difference between $0 and $135/month.&lt;/p&gt;

&lt;p&gt;Tap's architecture makes this explicit: &lt;strong&gt;pay the MCP overhead once during forge, then run at zero overhead forever.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How Tap's 40 MCP Tools Don't Blow Up Your Context
&lt;/h2&gt;

&lt;p&gt;The obvious concern: Tap ships 40 MCP tools. With 21 tools costing ~1,300 tokens of schema, 40 tools should cost ~2,500+. That's over 1% of a 200k context window before you even ask a question.&lt;/p&gt;

&lt;p&gt;Tap uses &lt;strong&gt;deferred tool loading&lt;/strong&gt;. Only 12 core tools load at session start (~600 tokens). The other 28 — forge, doctor, fix, trace, watch, explain — load on demand, only when the agent actually needs them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# What loads at session start (Tier 1 — always available)
tap.list   tap.run    tap.doctor   tap.nav
tap.click  tap.type   tap.eval     tap.find
tap.screenshot  tap.runtime  tap.pressKey  tap.upload

# What loads on demand (Tier 2 — disclosed via hints)
tap.fix    tap.explain   tap.trace    tap.watch
tap.refresh   tap.cookies   tap.wait   ...

# Forge tools — only load during forge sessions
forge.inspect   forge.draft   forge.verify   forge.save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same pattern the community arrived at independently:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Splitting tools into a tiny default set and a second on-demand pack, because dumping every possible tool into session start was where the waste really showed up."&lt;br&gt;
— Organic-Bid-8298, r/mcp&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Not Just Use CLI for Everything?
&lt;/h2&gt;

&lt;p&gt;Because authoring &lt;em&gt;requires&lt;/em&gt; tool discovery. When AI is figuring out how to scrape a site it's never seen before, it needs typed parameters, rich descriptions, and structured responses. That's what MCP does well.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The one thing MCP does well is when it's tightly integrated (like Claude Code's built-in tools) — that feels natural because they control both sides."&lt;br&gt;
— SmartYogurtcloset715 (8 upvotes), r/ClaudeAI&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tap controls both sides. The MCP server and the CLI are the same binary. The MCP tools call the same functions the CLI calls. The difference is &lt;em&gt;when&lt;/em&gt; each is used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Forge (one-time): MCP tools, because AI needs to discover and iterate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run (every time): CLI, because the program already exists&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Doctor (periodic): either — MCP for interactive diagnosis, CLI for scheduled health checks&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Implication for Browser Automation
&lt;/h2&gt;

&lt;p&gt;Most browser MCP tools are execution-layer tools. They run in the browser on every call. That's where the token cost comes from — not just schema overhead, but the entire page state (accessibility tree, screenshot bytes, console output) flowing into the context window on every interaction.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Every &lt;code&gt;browser_navigate&lt;/code&gt; + &lt;code&gt;browser_snapshot&lt;/code&gt; call costs ~1,500 tokens in JSON schema framing — even though the actual useful output is just a few lines of text."&lt;br&gt;
— BagNervous, r/ClaudeAI (Browser CLI author)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tap's browser tools exist in MCP for authoring only. During forge, AI uses &lt;code&gt;tap.nav&lt;/code&gt;, &lt;code&gt;tap.eval&lt;/code&gt;, &lt;code&gt;tap.screenshot&lt;/code&gt; to understand the page. After forge produces a &lt;code&gt;.tap.js&lt;/code&gt;, execution calls the browser directly — no MCP framing, no token overhead, no context window pollution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 1,500-token-per-call problem doesn't exist for tap.run. It's not an MCP call. It's a function call.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Health Contracts Catch What Pydantic Can't — semantic validation for scraper output&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Programs Beat Prompts — why AI should write code, not run it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Interface Protocol — 8 operations that replace every browser automation SDK&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>automation</category>
      <category>llm</category>
    </item>
    <item>
      <title>Facebook scrambles author names with Flexbox order — here's the 5-line diagnostic that proves it isn't custom fonts</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Wed, 22 Apr 2026 04:06:36 +0000</pubDate>
      <link>https://dev.to/leonting1010/facebook-scrambles-author-names-with-flexbox-order-heres-the-5-line-diagnostic-that-proves-it-ig2</link>
      <guid>https://dev.to/leonting1010/facebook-scrambles-author-names-with-flexbox-order-heres-the-5-line-diagnostic-that-proves-it-ig2</guid>
      <description>&lt;p&gt;A potential client posted on Reddit asking for a Facebook keyword-post scraper. Their budget: &lt;strong&gt;$500&lt;/strong&gt;. My first instinct after looking at the page was to say no.&lt;/p&gt;

&lt;p&gt;Here's what a naive scraper saw when it grabbed the first &lt;code&gt;[role="article"]&lt;/code&gt; on the search results page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oSodnprmmlffgfi1c3mSg0so0d000c0uh1l40llhe09n2991imm38opar · Shared with Public
In 24 months every serious website will talk. Get in before it's crowded.… See more
0:00 / 0:00
SNOWIE.AI
$67 Lifetime Deal!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;· Shared with Public&lt;/code&gt; and the post body are readable. But that first line — the one that should be the author's name — is gibberish. &lt;code&gt;Snowie.Ai&lt;/code&gt; rendered visually. &lt;code&gt;oSodnprm…&lt;/code&gt; returned by &lt;code&gt;textContent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I jumped to &lt;em&gt;"Facebook ships custom-font character remapping at scale — this is uncompetable, decline the gig."&lt;/em&gt; &lt;strong&gt;I was wrong.&lt;/strong&gt; Here's what the actual answer turned out to be, and why I had to write a diagnostic before I could see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven ways rendered text can escape textContent
&lt;/h2&gt;

&lt;p&gt;Before declaring a site uncompetable, you have to know what you're looking at. There are exactly seven mechanisms by which what a human sees on screen can diverge from what &lt;code&gt;Node.textContent&lt;/code&gt; returns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Defeat cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Selector mismatch (not actually anti-scraping — you grabbed the wrong node)&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;::before&lt;/code&gt; / &lt;code&gt;::after&lt;/code&gt; content rules&lt;/td&gt;
&lt;td&gt;Low — read computed style&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Flexbox &lt;code&gt;order&lt;/code&gt; reordering (DOM scrambled, CSS re-sorts visually)&lt;/td&gt;
&lt;td&gt;Low — sort children by computed &lt;code&gt;order&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Custom font glyph remapping (&lt;code&gt;.woff2&lt;/code&gt; rebinds codepoints)&lt;/td&gt;
&lt;td&gt;High — OCR pixels or reverse each session's font table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Unicode homoglyph substitution&lt;/td&gt;
&lt;td&gt;Low — NFKC + confusable normalize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Canvas pixel rendering (no DOM text at all)&lt;/td&gt;
&lt;td&gt;High — OCR only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;WebAssembly runtime decryption&lt;/td&gt;
&lt;td&gt;Extreme — reverse the WASM module, track session keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each requires a different defeat strategy with wildly different economics. #1 is free (fix your selector). #4 and #6 start at ~$15K/year to maintain. #7 is measured in tens of thousands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So the only useful question is: which one does this site use?&lt;/strong&gt; Without a diagnostic you're guessing — and guessing wrong costs you either a scraping contract you could have fulfilled, or a contract you over-promised on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-minute diagnostic
&lt;/h2&gt;

&lt;p&gt;I wrote a throwaway script that walks the first &lt;code&gt;[role="article"]&lt;/code&gt; and dumps the signals that separate the seven mechanisms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[role="article"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;font_family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;has_canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// Also check DevTools → Network → filter .wasm&lt;/span&gt;
  &lt;span class="na"&gt;child_sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text_len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it. The result killed every hypothesis except one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;font_family&lt;/code&gt; = &lt;code&gt;system-ui, -apple-system, sans-serif&lt;/code&gt; — Facebook is using the OS default font. &lt;strong&gt;Mechanism #4 ruled out&lt;/strong&gt; (no custom &lt;code&gt;.woff2&lt;/code&gt;, no glyph remapping).&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element. &lt;strong&gt;#6 ruled out.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No WASM in network traffic. &lt;strong&gt;#7 ruled out.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;innerText&lt;/code&gt; = &lt;code&gt;"Snowie.Ai\no\ns\no\ne\nt\nS\nd\nn\np\nr\n…"&lt;/code&gt;. Character-per-line. &lt;strong&gt;Flex-column newlines — that's the giveaway.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Child elements of the scrambled container: each contained 1–2 characters with a non-zero &lt;code&gt;order&lt;/code&gt; value like &lt;code&gt;order: 17&lt;/code&gt;, &lt;code&gt;order: 4&lt;/code&gt;, &lt;code&gt;order: 23&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the signature of &lt;strong&gt;mechanism #3 — Flexbox &lt;code&gt;order&lt;/code&gt; reordering&lt;/strong&gt;. Facebook splits author display names into individual single-character spans and gives each a scrambled &lt;code&gt;order&lt;/code&gt; value. The browser's flexbox layout re-sorts them for visual rendering. &lt;code&gt;textContent&lt;/code&gt; returns DOM order, which is randomized per render.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;only the author name&lt;/strong&gt; gets this treatment. Post body, engagement counts, &lt;code&gt;aria-label&lt;/code&gt;s, and timestamps are plain text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is ten lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When children are all single-character and at least one has a non-zero CSS order,&lt;/span&gt;
&lt;span class="c1"&gt;// sort by order, concat — that's the real text as the browser would paint it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unscramble&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;kids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;kids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;kids&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this helper wired in, &lt;code&gt;author_name&lt;/code&gt; extraction went from &lt;code&gt;"oSodnprm…"&lt;/code&gt; to &lt;code&gt;"Snowie.Ai"&lt;/code&gt;. Everything else — &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;like_count&lt;/code&gt;, &lt;code&gt;lang&lt;/code&gt; — was already plain. No OCR, no WASM reversal, no font-table reverse engineering. Ten lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No native post permalink.&lt;/strong&gt; Facebook search result cards don't expose a &lt;code&gt;/posts/&amp;lt;id&amp;gt;/&lt;/code&gt; href — the visible links are profile URLs with encrypted &lt;code&gt;__cft__&lt;/code&gt; tracking params. When no native ID is found, my scraper emits an &lt;code&gt;fb_&amp;lt;hash&amp;gt;&lt;/code&gt; id stable across runs for the same author+body combination. Downstream deduplication still works; you just can't deep-link back to the post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-loaded pagination.&lt;/strong&gt; The search page initially renders 1–3 results; the rest arrive as the user scrolls. A production scraper drives scroll events in a loop until &lt;code&gt;limit&lt;/code&gt; is satisfied or no new articles appear.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The generalizable lesson
&lt;/h2&gt;

&lt;p&gt;Every time I've been asked &lt;em&gt;"can you scrape site X?"&lt;/em&gt; and said no without running a diagnostic, I was wrong at least half the time. The reflex is understandable — the DOM returns garbage, you assume the worst — but the cost asymmetry is severe. &lt;strong&gt;Five minutes of running the seven-factor diagnostic versus walking away from a paying contract.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The protocol is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract &lt;code&gt;textContent&lt;/code&gt; of the element the human can read. If it matches visible text → mechanism #1, fix your selector.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;getComputedStyle(el).fontFamily&lt;/code&gt;. Points to a custom &lt;code&gt;.woff2&lt;/code&gt;? Suspect #4.&lt;/li&gt;
&lt;li&gt;Walk children. If many are single-character and have non-zero &lt;code&gt;order&lt;/code&gt;? Mechanism #3, unscramble with ten lines.&lt;/li&gt;
&lt;li&gt;Check for &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; siblings and WASM network requests. Both absent? #6 and #7 are ruled out.&lt;/li&gt;
&lt;li&gt;Only after this do you get to say "un-scrapable."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Facebook is not un-scrapable for keyword search. They've applied a low-cost obfuscation to one high-value field (the author name you might use for audience targeting) and left everything else alone. That's a reasonable product decision — enough friction to discourage casual scrapers, not enough to break accessibility tooling that depends on rendered text. As a side effect, someone who runs the diagnostic wins.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Question for the room: anyone here actually shipped against mechanism #4 (custom font glyph remap) or #7 (WASM decryption) in production? Curious what the maintenance cost looks like once you account for font table rotation / WASM session-key changes. Drop your war stories in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Original post with full JSON-LD metadata + Tap reference: &lt;a href="https://taprun.dev/blog/facebook-anti-scraping-flexbox-order.html" rel="noopener noreferrer"&gt;taprun.dev/blog/facebook-anti-scraping-flexbox-order&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>We Ran 15,000 Browser Automations. The Failure That Matters Most Is Invisible to Your Monitoring.</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Mon, 20 Apr 2026 01:37:47 +0000</pubDate>
      <link>https://dev.to/leonting1010/we-ran-15000-browser-automations-the-failure-that-matters-most-is-invisible-to-your-monitoring-34jo</link>
      <guid>https://dev.to/leonting1010/we-ran-15000-browser-automations-the-failure-that-matters-most-is-invisible-to-your-monitoring-34jo</guid>
      <description>&lt;p&gt;Half of our YouTube automation runs return 0 rows. Status: &lt;code&gt;ok&lt;/code&gt;. No exception thrown. No error logged. The program finishes in about 20 seconds and hands back an empty array, silently.&lt;/p&gt;

&lt;p&gt;We didn't know this until we looked at the traces.&lt;/p&gt;

&lt;p&gt;Over the past few months, Tap has executed 15,455 automation programs across real websites — Reddit, GitHub, Bilibili, Xiaohongshu, YouTube, Twitter, and more. The traces are structured JSON: site, tap name, status, rows returned, duration, error message if any. We analyzed all of them. What we found disagrees with the conventional mental model of how browser automations break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reliability Table Nobody Publishes
&lt;/h2&gt;

&lt;p&gt;Here are the actual numbers. Each row is a real platform. &lt;strong&gt;Hard error rate&lt;/strong&gt; is the fraction of runs that threw an exception. &lt;strong&gt;Silent empty rate&lt;/strong&gt; is the fraction of &lt;em&gt;successful&lt;/em&gt; runs (status: ok) that returned zero rows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Total runs&lt;/th&gt;
&lt;th&gt;Hard error %&lt;/th&gt;
&lt;th&gt;Silent empty %&lt;/th&gt;
&lt;th&gt;Effective failure %&lt;/th&gt;
&lt;th&gt;Avg duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twitter / X&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;154 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;437&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;td&gt;3,644 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reddit&lt;/td&gt;
&lt;td&gt;688&lt;/td&gt;
&lt;td&gt;13.8%&lt;/td&gt;
&lt;td&gt;6.4%&lt;/td&gt;
&lt;td&gt;19.4%&lt;/td&gt;
&lt;td&gt;4,075 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xiaohongshu&lt;/td&gt;
&lt;td&gt;361&lt;/td&gt;
&lt;td&gt;15.8%&lt;/td&gt;
&lt;td&gt;6.6%&lt;/td&gt;
&lt;td&gt;21.5%&lt;/td&gt;
&lt;td&gt;9,054 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bilibili&lt;/td&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;30.1%&lt;/td&gt;
&lt;td&gt;18.2%&lt;/td&gt;
&lt;td&gt;43.1%&lt;/td&gt;
&lt;td&gt;2,666 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weibo&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;36.8%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;36.8%&lt;/td&gt;
&lt;td&gt;4,644 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YouTube&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;30.6%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20,273 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GitHub and Twitter are near-zero failure. YouTube is the opposite: two out of three runs either throw an error or return nothing. The 50% silent empty rate is more alarming than the 30.6% hard error rate — at least hard errors are visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failure Mode You're Not Tracking
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised us most. We expected "element not found" to be the dominant failure. The conventional model: selector breaks, automation throws, you fix the selector. Obvious, visible, actionable.&lt;/p&gt;

&lt;p&gt;The actual numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Element not found&lt;/strong&gt; (explicit selector failure): &lt;strong&gt;5 occurrences&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cannot read properties of undefined (reading 'url')&lt;/strong&gt; (implicit structural failure): &lt;strong&gt;176 occurrences&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ratio is 35:1 in favor of the failure mode your monitoring doesn't catch.&lt;/p&gt;

&lt;p&gt;What does &lt;code&gt;Cannot read properties of undefined (reading 'url')&lt;/code&gt; actually mean? The selector found something. The extraction ran. The automation didn't crash during navigation. It returned data — a list of objects — but the objects no longer have a &lt;code&gt;url&lt;/code&gt; field. The downstream code hits undefined and throws.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;structural drift failure&lt;/strong&gt;, not a selector failure. The DOM element is there. The page loaded. The program traversed the right nodes. But the shape of the data those nodes return has changed — a field that was always present quietly stopped being present.&lt;/p&gt;

&lt;p&gt;The sites affected, in order of frequency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bilibili (videos, articles, analytics, benchmark, content-ideas, stats, trending)&lt;/li&gt;
&lt;li&gt;Algora bounties&lt;/li&gt;
&lt;li&gt;IssueHunt bounties&lt;/li&gt;
&lt;li&gt;Douyin (search, hot)&lt;/li&gt;
&lt;li&gt;Zhihu search&lt;/li&gt;
&lt;li&gt;X/Twitter (notifications, trending)&lt;/li&gt;
&lt;li&gt;Xiaohongshu search&lt;/li&gt;
&lt;li&gt;Weibo search&lt;/li&gt;
&lt;li&gt;Baidu hot&lt;/li&gt;
&lt;li&gt;Hacker News hot&lt;/li&gt;
&lt;li&gt;ProductHunt forum comments&lt;/li&gt;
&lt;li&gt;TechCrunch latest&lt;/li&gt;
&lt;li&gt;Ars Technica news&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list spans Chinese platforms, Western platforms, social networks, news sites, and developer bounty boards. The failure mode is not platform-specific. It's inherent to how browser automation interacts with any site that changes its rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Your Monitoring Doesn't See This
&lt;/h2&gt;

&lt;p&gt;Consider what's happening at the infrastructure layer when this failure occurs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP response: 200&lt;/li&gt;
&lt;li&gt;Page loaded successfully: yes&lt;/li&gt;
&lt;li&gt;Navigation completed: yes&lt;/li&gt;
&lt;li&gt;Automation process exited: 0&lt;/li&gt;
&lt;li&gt;Exception thrown: eventually — but only after the extraction, when downstream code accesses the malformed object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most monitoring stacks see a successful process exit followed by an application exception. But the harder version is when the object &lt;em&gt;does&lt;/em&gt; have a &lt;code&gt;url&lt;/code&gt; field — it just points to something different. A related item section. A sponsored result. A pagination link that got included in the data array.&lt;/p&gt;

&lt;p&gt;In those cases: status &lt;code&gt;ok&lt;/code&gt;, rows returned, no exception, wrong data. Pydantic passes. Row count checks pass. Prometheus reports a healthy process. OTel has nothing to report. The only signal is semantic: &lt;em&gt;these URLs aren't the URLs you wanted.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Platform Reliability Pattern
&lt;/h2&gt;

&lt;p&gt;GitHub and Twitter have published APIs that their web UIs reflect. A GitHub repository page structure is stable because it's owned by the same team that maintains the underlying data model.&lt;/p&gt;

&lt;p&gt;Bilibili, Douyin, Xiaohongshu, and Weibo run aggressive A/B experiments on their rendering layer — sometimes multiple experiments simultaneously for different user cohorts. The same page, loaded twice in the same session, can return different DOM structures. The &lt;code&gt;url&lt;/code&gt; field on a video card might be in &lt;code&gt;item.url&lt;/code&gt; in one experiment variant and &lt;code&gt;item.jumpUrl&lt;/code&gt; in another.&lt;/p&gt;

&lt;p&gt;YouTube lands in between for a different reason: aggressive anti-bot measures that return empty results instead of blocking requests. A request that would return a 429 or CAPTCHA on a naive scraper returns 200 with an empty content container on a logged-out browser session. Status: ok. Rows: 0. Duration: 20 seconds of wasted compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Catches This, and What Doesn't
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Catches hard error?&lt;/th&gt;
&lt;th&gt;Catches silent empty?&lt;/th&gt;
&lt;th&gt;Catches wrong data (right shape)?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Process monitoring&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;tr&gt;
&lt;td&gt;Pydantic / type validation&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row count threshold&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;tr&gt;
&lt;td&gt;Health contracts (range + pattern + drift)&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;tr&gt;
&lt;td&gt;Structural fingerprinting&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Signals change, not interpretation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only layer that catches all three failure classes is a contract that validates semantics — not just shape. A &lt;code&gt;min_rows&lt;/code&gt; check catches silent empties. A &lt;code&gt;pattern&lt;/code&gt; check on URLs catches wrong-source data. A &lt;code&gt;drift&lt;/code&gt; check catches distribution shifts that look valid but represent changed behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Treat silent empties as first-class failures.&lt;/strong&gt; A run returning zero rows should be suspicious by default. Most automations that legitimately return zero rows are edge cases. Most that return zero rows unexpectedly are broken. The difference is detectable with a &lt;code&gt;min_rows&lt;/code&gt; contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fingerprint before running, not after.&lt;/strong&gt; The structural drift that causes &lt;code&gt;Cannot read properties of undefined&lt;/code&gt; is detectable in the DOM before you run your extraction logic. A fingerprint check is cheaper than a full tap execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat Chinese platforms as a separate reliability tier.&lt;/strong&gt; The A/B experiment cadence is genuinely different. A tap targeting Bilibili needs shorter contract drift windows and more frequent health checks than one targeting GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duration is a signal.&lt;/strong&gt; Our YouTube taps average 20 seconds per run and fail 65% of the time. That's not slow extraction — that's waiting for content that's not coming. A timeout contract that fires at 8 seconds would catch most of these early.&lt;/p&gt;




&lt;p&gt;The trace data from 15,455 runs is the most honest answer we have to "what actually breaks in browser automation?"&lt;/p&gt;

&lt;p&gt;The answer: silent structural drift, not explicit selector failure. The sites that change fastest break most. The failures that matter most are the ones that look like success.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://taprun.dev" rel="noopener noreferrer"&gt;Tap&lt;/a&gt; — browser automation programs that run forever.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Search arXiv in One Command — No API Key, No Tokens</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Tue, 07 Apr 2026 09:46:59 +0000</pubDate>
      <link>https://dev.to/leonting1010/search-arxiv-in-one-command-no-api-key-no-tokens-54ib</link>
      <guid>https://dev.to/leonting1010/search-arxiv-in-one-command-no-api-key-no-tokens-54ib</guid>
      <description>&lt;p&gt;Keeping up with AI research is exhausting. New papers drop daily. Most "paper discovery" tools require an account, burn API tokens on every search, or give you a bloated UI when all you wanted was a list.&lt;/p&gt;

&lt;p&gt;Here's what I use instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli arxiv search &lt;span class="nt"&gt;--keyword&lt;/span&gt; &lt;span class="s2"&gt;"LLM"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; published &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: 20 papers sorted newest-first, with title, authors, published date, abstract, and URL — in under 2 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No account. No API key. No AI tokens consumed.&lt;/strong&gt; First run downloads a ~30MB binary and caches it; every subsequent call is instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;The arXiv Atom API has been public and stable for 15 years. &lt;code&gt;arxiv/search&lt;/code&gt; is a &lt;a href="https://github.com/LeonTing1010/tap-skills" rel="noopener noreferrer"&gt;Tap skill&lt;/a&gt; — a 20-line deterministic program that calls it directly. AI wrote it once. It runs forever at $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unix Pipeline Model
&lt;/h2&gt;

&lt;p&gt;Every Tap skill is a composable Unix filter. Data flows as JSON:&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="c"&gt;# Search only&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli arxiv search &lt;span class="nt"&gt;--keyword&lt;/span&gt; &lt;span class="s2"&gt;"RAG"&lt;/span&gt;

&lt;span class="c"&gt;# Search → sort by date → filter recent → display&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli arxiv search &lt;span class="nt"&gt;--keyword&lt;/span&gt; &lt;span class="s2"&gt;"RAG"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; published &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli filter &lt;span class="nt"&gt;--field&lt;/span&gt; published &lt;span class="nt"&gt;--gt&lt;/span&gt; &lt;span class="s2"&gt;"2025-01-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each command reads JSON from stdin, writes JSON to stdout. Exactly how Unix tools work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use It in CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions — daily paper digest&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Paper digest&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;npx -y @taprun/cli arxiv search --keyword "LLM agents" \&lt;/span&gt;
      &lt;span class="s"&gt;| npx -y @taprun/cli sort --field published \&lt;/span&gt;
      &lt;span class="s"&gt;| npx -y @taprun/cli limit --n 5 \&lt;/span&gt;
      &lt;span class="s"&gt;&amp;gt; papers.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  200+ Skills, Same Pattern
&lt;/h2&gt;

&lt;p&gt;arXiv is one of 200+ community skills that follow the same pattern: call an API, return structured rows, compose with any other skill.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;arxiv/search --keyword X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Papers matching keyword&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;github/trending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trending repos today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reddit/search --keyword X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Posts matching keyword&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stackoverflow/hot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hot questions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Browse and contribute: &lt;a href="https://github.com/LeonTing1010/tap-skills" rel="noopener noreferrer"&gt;github.com/LeonTing1010/tap-skills&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Try it now — no install required, works on any machine with Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli arxiv search &lt;span class="nt"&gt;--keyword&lt;/span&gt; &lt;span class="s2"&gt;"your topic"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; published &lt;span class="se"&gt;\&lt;/span&gt;
  | npx &lt;span class="nt"&gt;-y&lt;/span&gt; @taprun/cli table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;a href="https://taprun.dev" rel="noopener noreferrer"&gt;taprun.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>16 Comments, 6 Insights: Using HN and Reddit as a Positioning Lab</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Mon, 06 Apr 2026 05:18:09 +0000</pubDate>
      <link>https://dev.to/leonting1010/16-comments-6-insights-using-hn-and-reddit-as-a-positioning-lab-46oo</link>
      <guid>https://dev.to/leonting1010/16-comments-6-insights-using-hn-and-reddit-as-a-positioning-lab-46oo</guid>
      <description>&lt;p&gt;I spent an afternoon writing 16 comments across Hacker News and Reddit. Not to promote anything — to &lt;strong&gt;test which pain points actually resonate with developers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The result: 6 content principles I now use to decide what to build, what to write about, and how to position my product. Here's the method.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Method: Comments as Micro-Experiments
&lt;/h2&gt;

&lt;p&gt;The premise is simple: &lt;strong&gt;a comment is the cheapest possible A/B test.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writing a blog post takes hours. A landing page rewrite takes days. A comment takes 2 minutes. If it gets upvoted, the angle works. If it's ignored, you saved yourself a blog post nobody would read.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Find hot posts in your domain (automation, scraping, developer tools, AI)&lt;/li&gt;
&lt;li&gt;Write a comment that tests a specific angle — one pain point, one insight&lt;/li&gt;
&lt;li&gt;Track which angles get traction&lt;/li&gt;
&lt;li&gt;Turn validated angles into blog posts and landing page copy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I posted across 10 subreddits and HN front-page posts spanning AI, infrastructure, open source, and developer tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 Insights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  #1 Silent failure is the universal pain
&lt;/h3&gt;

&lt;p&gt;I commented on Gallery-dl's DMCA move (HN front page) and r/webscraping's "endgame for scraping" (104 upvotes). Both times, the angle that resonated was: &lt;strong&gt;the hard part isn't writing a scraper — it's knowing when it breaks.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Most scrapers fail silently — they return empty arrays for days before anyone notices."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt; Lead with the maintenance problem, not the creation problem. Everyone can build a scraper. Nobody can keep it running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Health contracts. Every program defines what "working" means: minimum rows, required fields. &lt;code&gt;tap doctor&lt;/code&gt; checks all programs in one command. When something breaks, you know in seconds — not weeks.&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;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;min_rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;non_empty&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;title"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;$ tap doctor&lt;/span&gt;
&lt;span class="s"&gt;hackernews/hot    ✔ ok     30 rows&lt;/span&gt;
&lt;span class="s"&gt;reddit/hot        ✘ fail   0 rows — selector changed&lt;/span&gt;
  &lt;span class="s"&gt;↳ auto-healing...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  #2 Cost anxiety is real and specific
&lt;/h3&gt;

&lt;p&gt;Caveman hit 727 points — a post about reducing LLM token usage. Nanocode (177 points) was about self-hosting Claude Code to understand the real cost. Developers aren't just curious about AI costs — they're &lt;strong&gt;anxious&lt;/strong&gt; about them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt; Use exact numbers. "$1.05 per run" and "300x cheaper" land. "More affordable" doesn't. Developers think in math, not adjectives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; The compiler model. AI runs once at authoring time (~$0.15), produces a deterministic program, and every subsequent execution is $0. Fifty daily automations: $18,000/year with AI agents vs ~$60/year with compiled programs.&lt;/p&gt;

&lt;h3&gt;
  
  
  #3 "Open-source alternative" is not a value prop
&lt;/h3&gt;

&lt;p&gt;The Modo post ("open-source alternative to Cursor and Windsurf") had only 2 comments despite being on the front page. My feedback: &lt;strong&gt;users don't switch tools for ideology — they switch for workflow improvements.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The README should lead with a concrete before/after: 'In Cursor you do X in 5 steps, in Modo you do it in 1.' That's what converts users."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt; Show the delta, not the category. "I'm like X but open source" tells users nothing about why they should switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Concrete comparison. Browser Use: $0.50–$2.00/run, 60–95% reliability, 30–120s. Tap: $0/run, 100% deterministic, 1–5s. Same task, measurable difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  #4 Local-first is having a moment
&lt;/h3&gt;

&lt;p&gt;Three unrelated posts all trended around the same theme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 on iPhone&lt;/strong&gt; (496 points) — on-device AI inference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-dependency browser IDE&lt;/strong&gt; (r/opensource) — works offline, no npm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nomad offline media server&lt;/strong&gt; (r/selfhosted) — works without internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers are increasingly allergic to tools that phone home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt; "Runs on your machine, works offline" is a feature worth highlighting, not an implementation detail to bury.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Tap programs are plain &lt;code&gt;.tap.js&lt;/code&gt; files that execute locally. No API calls at runtime, no data leaving your device, no cloud dependency. They work on a plane, in a cabin, wherever your laptop goes.&lt;/p&gt;

&lt;h3&gt;
  
  
  #5 Legal pressure on scraping is accelerating
&lt;/h3&gt;

&lt;p&gt;Gallery-dl's DMCA notice trended on &lt;em&gt;both&lt;/em&gt; HN and r/programming simultaneously. The pattern: &lt;strong&gt;open-source scraping tools face increasing legal pressure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt; API-first data access is both technically superior and legally safer. Position accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;code&gt;tap.fetch()&lt;/code&gt; calls site APIs directly — structured JSON, stable endpoints, no DOM parsing. Only falls back to browser rendering when no API exists. Less breakage, less legal surface area.&lt;/p&gt;

&lt;h3&gt;
  
  
  #6 Infrastructure beats features
&lt;/h3&gt;

&lt;p&gt;Switzerland's 25 Gbit internet (315 points, 249 comments) wasn't about speed — it was about &lt;strong&gt;structural fairness&lt;/strong&gt;. Open fiber access vs. local monopolies.&lt;/p&gt;

&lt;p&gt;The parallel to automation tooling: AI agents at $1/run create a cost barrier. Deterministic programs at $0 are infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt; Frame your tool as infrastructure people own, not a service they rent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Every &lt;code&gt;.tap.js&lt;/code&gt; is a file you own. Git-versionable, diffable, composable. Cancel your subscription and your programs keep running. No vendor lock-in, no API keys required at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Playbook
&lt;/h2&gt;

&lt;p&gt;If you're building a developer tool and struggling with positioning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't start with a landing page.&lt;/strong&gt; Start with 10 comments on relevant posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each comment tests one angle.&lt;/strong&gt; One pain point, one insight, one framing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upvotes = validation.&lt;/strong&gt; High-scoring posts where your comment resonates = confirmed pain point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silence = signal too.&lt;/strong&gt; If nobody engages with your angle, it's not a pain point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn validated angles into content.&lt;/strong&gt; Blog post from the best angle. Landing page copy from the specific phrases that worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never link your product in comments.&lt;/strong&gt; Share expertise. Build credibility. The product link lives on your profile.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Comments are conversations. Conversations reveal what people actually care about. That's worth more than any amount of competitor analysis.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tool I used to validate these insights: &lt;a href="https://taprun.dev" rel="noopener noreferrer"&gt;Tap&lt;/a&gt; turns AI into a compiler for browser automation. AI writes a program once, then it runs forever at $0. The positioning came from the comments. The product came from the pain.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>product</category>
      <category>startup</category>
      <category>writing</category>
    </item>
    <item>
      <title>Programs Beat Prompts: Why AI Should Write Code, Not Run It</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Mon, 06 Apr 2026 05:04:19 +0000</pubDate>
      <link>https://dev.to/leonting1010/programs-beat-prompts-why-ai-should-write-code-not-run-it-3gde</link>
      <guid>https://dev.to/leonting1010/programs-beat-prompts-why-ai-should-write-code-not-run-it-3gde</guid>
      <description>&lt;p&gt;Every AI browser agent works the same way: send a prompt, burn tokens, get a result. Next time you need the same result? Send the prompt again. Burn more tokens.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;prompt loop&lt;/strong&gt;, and it's why AI automation is expensive, unreliable, and slow.&lt;/p&gt;

&lt;p&gt;There's a better model: &lt;strong&gt;AI writes a program once, then the program runs forever at zero cost.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt Loop Problem
&lt;/h2&gt;

&lt;p&gt;AI browser agents like Browser Use, Stagehand, and computer-use tools are impressive demos. Point an LLM at a website, tell it what to do, watch it click around. Magic.&lt;/p&gt;

&lt;p&gt;Until you need it to work reliably. At scale. Every day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The program cost $1.05 to run. So doing it at any scale quickly becomes a little bit silly."&lt;br&gt;
— rozap, Hacker News&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The math is brutal. An AI agent that scrapes one site costs ~$1 per run. Run it daily across 50 sites and you're spending &lt;strong&gt;$1,500/month&lt;/strong&gt; on data collection that a deterministic script would do for free.&lt;/p&gt;

&lt;p&gt;But cost isn't even the worst part. &lt;strong&gt;Reliability is.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If each step has a .95 chance of completing successfully, after not very many steps you have a pretty small overall probability of success."&lt;br&gt;
— rozap, Hacker News&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;95% per step sounds good. A 10-step workflow? 60% overall. Twenty steps? 36%. Every AI call is a coin flip weighted slightly in your favor — but over enough steps, the house always wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compiler Model
&lt;/h2&gt;

&lt;p&gt;There's a pattern from 60 years of computer science that solves this: &lt;strong&gt;compilation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A compiler reads high-level code once, produces optimized machine code, and that machine code runs billions of times at zero marginal cost. The compiler is expensive. The output is free.&lt;/p&gt;

&lt;p&gt;Apply this to browser automation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Prompt model (interpreter)&lt;/th&gt;
&lt;th&gt;Program model (compiler)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI cost per run&lt;/td&gt;
&lt;td&gt;$0.50 – $2.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability per run&lt;/td&gt;
&lt;td&gt;60 – 95%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% deterministic&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;30 – 120s (LLM thinking)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 – 5s (direct execution)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI usage&lt;/td&gt;
&lt;td&gt;Every run&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Only at authoring time&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The insight: &lt;strong&gt;AI is the compiler, not the runtime.&lt;/strong&gt; Use AI to understand the interface, write a deterministic program, and then execute that program forever without AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: AI observes the interface (the "compile" step)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap forge &lt;span class="s2"&gt;"get trending repos from GitHub"&lt;/span&gt;
Inspecting github.com...
Found API: api.github.com/search/repositories
Verifying: 25 rows, all fields present
✔ Saved: github/trending.tap.js

&lt;span class="c"&gt;# Step 2: Program runs forever at $0 (the "execute" step)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap github trending
25 rows &lt;span class="o"&gt;(&lt;/span&gt;890ms&lt;span class="o"&gt;)&lt;/span&gt; Cost: &lt;span class="nv"&gt;$0&lt;/span&gt;.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command uses AI. Every subsequent run is pure code — no LLM, no tokens, no variability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Programs Win
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Deterministic means debuggable
&lt;/h3&gt;

&lt;p&gt;When a prompt fails, you get "the AI didn't understand the page." When a program fails, you get a stack trace, a line number, and a selector that changed. One is a mystery. The other is a bug you can fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Programs compose
&lt;/h3&gt;

&lt;p&gt;Prompts are isolated. Each one starts fresh, with no memory of what came before. Programs call other programs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This tap calls two other taps and combines the results&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tap&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tap&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composition is free. No extra tokens. No prompt engineering to maintain context across steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Programs have contracts
&lt;/h3&gt;

&lt;p&gt;A prompt returns whatever the AI decides. A program has a health contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// must return at least 10 results&lt;/span&gt;
  &lt;span class="nx"&gt;non_empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;// title field can't be empty&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the site changes and the program breaks, the contract catches it immediately. No silent failure. No stale data for days.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Programs version naturally
&lt;/h3&gt;

&lt;p&gt;A .tap.js file is just JavaScript. It goes in Git. You get diffs, blame, history, rollback. Try doing that with a prompt chain stored in a vector database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Do Need AI Again
&lt;/h2&gt;

&lt;p&gt;Programs aren't magic. Websites change. When they do:&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="nv"&gt;$ &lt;/span&gt;tap doctor
github/trending   ✔ ok     25 rows
reddit/hot        ✘ fail   0 rows  — selector changed

&lt;span class="c"&gt;# Doctor detected the break. Re-forge just that one:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap doctor &lt;span class="nt"&gt;--auto&lt;/span&gt;
Re-forging reddit/hot...
✔ Fixed: reddit/hot.tap.js &lt;span class="o"&gt;(&lt;/span&gt;new selectors&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI is called once to fix the program. Then it runs at $0 again until the next change. You pay for intelligence only when the world changes — which is 1% of the time, not 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;For a real workload of 50 automations running daily:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Prompt-per-run&lt;/th&gt;
&lt;th&gt;Programs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily AI cost&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly AI cost&lt;/td&gt;
&lt;td&gt;$1,500&lt;/td&gt;
&lt;td&gt;~$5 (occasional re-forge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annual AI cost&lt;/td&gt;
&lt;td&gt;$18,000&lt;/td&gt;
&lt;td&gt;~$60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;60 – 95%&lt;/td&gt;
&lt;td&gt;100% (until site changes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed per run&lt;/td&gt;
&lt;td&gt;30 – 120s&lt;/td&gt;
&lt;td&gt;1 – 5s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;300x cost reduction. 20x faster. Deterministic. The math doesn't require a sales pitch.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install (macOS / Linux)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://taprun.dev/install.sh | sh

&lt;span class="c"&gt;# Forge your first program&lt;/span&gt;
tap forge &lt;span class="s2"&gt;"get top stories from Hacker News"&lt;/span&gt;

&lt;span class="c"&gt;# Run it forever at $0&lt;/span&gt;
tap hackernews hot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://taprun.dev/getting-started.html" rel="noopener noreferrer"&gt;Getting started&lt;/a&gt; · &lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · 200+ community taps included&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your AI Browser Agent Costs $3,600/month. Here's How to Make It $0</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Sun, 05 Apr 2026 15:08:47 +0000</pubDate>
      <link>https://dev.to/leonting1010/your-ai-browser-agent-costs-3600month-heres-how-to-make-it-0-4228</link>
      <guid>https://dev.to/leonting1010/your-ai-browser-agent-costs-3600month-heres-how-to-make-it-0-4228</guid>
      <description>&lt;p&gt;A developer recently documented burning through &lt;strong&gt;180 million tokens per month&lt;/strong&gt; — $3,600 — running AI browser agents. That's not a typo.&lt;/p&gt;

&lt;p&gt;The browser-use community (78K GitHub stars) is full of users asking the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have a recurring task meant for webscraping to be done every 5 min. I do not want to use too many tokens. Is it possible to repeat the tasks?" — &lt;a href="https://github.com/browser-use/browser-use/discussions/494" rel="noopener noreferrer"&gt;browser-use #494&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"My business scenario requires solidifying the agent's execution process into a tool. I noticed &lt;code&gt;save_as_playwright_script&lt;/code&gt; is commented out." — &lt;a href="https://github.com/browser-use/browser-use/discussions/4519" rel="noopener noreferrer"&gt;browser-use #4519&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Running the default task took &lt;strong&gt;12 minutes&lt;/strong&gt; on M3 Max, 36GB RAM" — &lt;a href="https://github.com/browser-use/browser-use/discussions/957" rel="noopener noreferrer"&gt;browser-use #957&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem is architectural: &lt;strong&gt;every run uses AI tokens&lt;/strong&gt;, even when you're doing the exact same thing for the 1,000th time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Interpreter vs. Compiler Model
&lt;/h2&gt;

&lt;p&gt;Today's browser agents work like interpreters — AI reasons about every click, every scroll, every form fill, every single time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interpreter (browser-use, Stagehand, Operator):
  Run 1:    AI reads page → decides action → executes    ($0.01)
  Run 2:    AI reads page → decides action → executes    ($0.01)
  Run 100:  AI reads page → decides action → executes    ($0.01)
  Run 1000: AI reads page → decides action → executes    ($0.01)
  Total: $10.00 (and growing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what if AI could &lt;strong&gt;compile&lt;/strong&gt; the workflow once, then replay it forever?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compiler approach:
  Run 1:    AI inspects page → generates program          ($0.04, one-time)
  Run 2:    Program runs deterministically                 ($0.00)
  Run 100:  Program runs deterministically                 ($0.00)
  Run 1000: Program runs deterministically                 ($0.00)
  Total: $0.04 (forever)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't hypothetical. &lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;Tap&lt;/a&gt; implements this exact pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;forge inspect&lt;/code&gt;&lt;/strong&gt; — Analyzes the page (framework, SSR state, APIs, DOM structure). Zero AI tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI generates a &lt;code&gt;.tap.js&lt;/code&gt; program&lt;/strong&gt; — One-time cost (~$0.04).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tap run&lt;/code&gt;&lt;/strong&gt; — Executes the program forever. $0.00 per run.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why API-First Beats DOM Replay
&lt;/h2&gt;

&lt;p&gt;Most record-and-replay tools (including browser-use's &lt;a href="https://github.com/browser-use/workflow-use" rel="noopener noreferrer"&gt;workflow-use&lt;/a&gt;) capture DOM interactions — clicks, typing, scrolling. This breaks when the UI changes.&lt;/p&gt;

&lt;p&gt;The better approach: &lt;strong&gt;extract via API when possible, DOM only as fallback.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most modern websites have internal APIs (Next.js &lt;code&gt;__NEXT_DATA__&lt;/code&gt;, Nuxt SSR state, REST endpoints). Calling the API directly is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100x more reliable than simulating clicks&lt;/li&gt;
&lt;li&gt;Immune to UI redesigns&lt;/li&gt;
&lt;li&gt;Faster (no rendering needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, getting Hacker News front page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DOM approach (fragile):&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.athing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// API approach (robust):&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://hacker-news.firebaseio.com/v0/topstories.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;AI Agent (per run)&lt;/th&gt;
&lt;th&gt;Compiled Program (per run)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0.003–0.01&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;12 min (reported)&lt;/td&gt;
&lt;td&gt;5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Varies (AI hallucinations)&lt;/td&gt;
&lt;td&gt;Deterministic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;1K–10K per action&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 100 runs/day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI agent: &lt;strong&gt;$30–300/month&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Compiled program: &lt;strong&gt;$0.04 total&lt;/strong&gt; (one-time forge cost)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're running the same browser task more than once, you're overpaying by 100–1000x. The future isn't smarter agents — it's agents that are &lt;strong&gt;smart once&lt;/strong&gt; and produce deterministic programs.&lt;/p&gt;

&lt;p&gt;Token prices are falling 10x/year. But $0 will always beat any price.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;Tap&lt;/a&gt; is open source. 208 pre-built programs across 77 sites. One binary, zero dependencies.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Try it: &lt;a href="https://taprun.dev" rel="noopener noreferrer"&gt;taprun.dev&lt;/a&gt; | &lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your Scraper Is Broken Right Now. You Just Don't Know It Yet.</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Sat, 04 Apr 2026 13:18:29 +0000</pubDate>
      <link>https://dev.to/leonting1010/your-scraper-is-broken-right-now-you-just-dont-know-it-yet-38f5</link>
      <guid>https://dev.to/leonting1010/your-scraper-is-broken-right-now-you-just-dont-know-it-yet-38f5</guid>
      <description>&lt;p&gt;Somewhere in your infrastructure, a scraper is returning empty arrays. Your dashboard shows stale numbers. A report your team relies on has been wrong since Tuesday.&lt;/p&gt;

&lt;p&gt;You won't find out until someone complains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Failure Problem
&lt;/h2&gt;

&lt;p&gt;Most scrapers don't crash loudly. They fail quietly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Instead of throwing an error when a page structure changes, they return empty arrays... A scraper that fails silently poisons your data for days or weeks before anyone notices."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This happens because scrapers have no &lt;strong&gt;health contract&lt;/strong&gt;. They extract whatever they find — and when the site changes, "whatever they find" is nothing. No error. No alert. Just empty data flowing downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintenance Tax
&lt;/h2&gt;

&lt;p&gt;When you do notice, you're back to fixing selectors. Again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Every time a website redesigns or updates their layout, I'm manually fixing selectors and rewriting parts of the workflow. It's eating up hours every month."&lt;/p&gt;

&lt;p&gt;"Maintaining tests can take up to 50% of the time for QA test automation engineers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The loop is always the same: build automation → site changes → selectors break → spend hours fixing → repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Agent Tax
&lt;/h2&gt;

&lt;p&gt;AI browser agents promise to solve this by re-interpreting the page every run. But they introduce two new problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cost compounds.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The program cost $1.05 to run. So doing it at any scale quickly becomes a little bit silly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Reliability degrades at each step.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If each step has a .95 chance of completing successfully, after not very many steps you have a pretty small overall probability of success."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;95% per step sounds great. But a 10-step workflow is 60% overall. AI agents trade one problem (brittle selectors) for another (probabilistic failure).&lt;/p&gt;

&lt;h2&gt;
  
  
  A Different Approach: Health Contracts
&lt;/h2&gt;

&lt;p&gt;What if your automation had a &lt;strong&gt;contract&lt;/strong&gt; that defined what "healthy" looks like?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Built into every program&lt;/span&gt;
&lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min_rows&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;// must return at least 5 results&lt;/span&gt;
  &lt;span class="nx"&gt;non_empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;// "title" field must never be empty&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now instead of silently returning empty arrays, the system &lt;em&gt;knows&lt;/em&gt; when something is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tap doctor
&lt;span class="go"&gt;hackernews/hot    ✓ ok     30 rows  (245ms)
google/trends     ✗ fail   0 rows   min_rows: expected ≥5, got 0
github/trending   ✓ ok     25 rows  (1.2s)
bbc/news          ✗ fail   3 rows   min_rows: expected ≥5, got 3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two failures caught. Before your data went bad. Before anyone complained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch: Real-Time Change Detection
&lt;/h2&gt;

&lt;p&gt;Health checks catch breakage. But what about legitimate changes?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I built Site Spy after missing a visa appointment slot because a government page changed and I didn't notice for two weeks."&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tap watch hackernews hot &lt;span class="nt"&gt;--every&lt;/span&gt; 10m
&lt;span class="go"&gt;2026-04-04T10:00  +added   "Show HN: Tap"  score=342
2026-04-04T10:10  +added   "Rust 2.0 announced"  score=128
2026-04-04T10:10  -removed "Old post fell off"  score=12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your program on an interval, diff the results, output only what changed. Pipe it to a file, Slack webhook, or another program.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Self-Healing Loop
&lt;/h2&gt;

&lt;p&gt;Put it all together:&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="c"&gt;# 1. AI writes a deterministic program once&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap forge &lt;span class="s2"&gt;"scrape Hacker News top stories"&lt;/span&gt;
✓ Saved: hackernews/hot.tap.js

&lt;span class="c"&gt;# 2. Run forever at $0&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap hackernews hot
30 rows &lt;span class="o"&gt;(&lt;/span&gt;245ms&lt;span class="o"&gt;)&lt;/span&gt; Cost: &lt;span class="nv"&gt;$0&lt;/span&gt;.00

&lt;span class="c"&gt;# 3. Watch for data changes&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap watch hackernews hot &lt;span class="nt"&gt;--every&lt;/span&gt; 1h

&lt;span class="c"&gt;# 4. Daily health check&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap doctor &lt;span class="nt"&gt;--schedule&lt;/span&gt; &lt;span class="s2"&gt;"0 6 * * *"&lt;/span&gt;

&lt;span class="c"&gt;# 5. Auto-heal when something breaks&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;tap doctor &lt;span class="nt"&gt;--auto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop: &lt;strong&gt;forge → run → watch → doctor → heal → run&lt;/strong&gt;. You sleep. Your automations don't stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;The key insight: &lt;strong&gt;AI should run at authoring time, not at runtime.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forge&lt;/strong&gt; uses AI once to write a deterministic program&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;/strong&gt; executes it with zero AI — $0 per execution, 100% deterministic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doctor&lt;/strong&gt; detects breakage via health contracts — no AI needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heal&lt;/strong&gt; re-invokes AI only when the site actually changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;99% of runs need zero AI. You only pay for intelligence when the world changes.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;Tap&lt;/a&gt; is open source. 195+ pre-built automations included. &lt;a href="https://taprun.dev/getting-started.html" rel="noopener noreferrer"&gt;Getting started takes 2 minutes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://taprun.dev/blog/your-scraper-is-broken.html" rel="noopener noreferrer"&gt;taprun.dev/blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>automation</category>
      <category>ai</category>
      <category>scraping</category>
    </item>
    <item>
      <title>Programs Beat Prompts: How Tap Turns AI into a Compiler for Browser Automation</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Thu, 02 Apr 2026 07:06:51 +0000</pubDate>
      <link>https://dev.to/leonting1010/programs-beat-prompts-how-tap-turns-ai-into-a-compiler-for-browser-automation-oab</link>
      <guid>https://dev.to/leonting1010/programs-beat-prompts-how-tap-turns-ai-into-a-compiler-for-browser-automation-oab</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every time you ask an AI agent to do something in a browser, it costs money and time. Click here, type there, extract that — the AI figures it out from scratch every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if AI only had to figure it out once?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tap: The Compiler Approach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://leonting1010.github.io/tap/" rel="noopener noreferrer"&gt;Tap&lt;/a&gt; is a protocol + toolchain that turns AI's interface operations into deterministic programs (&lt;code&gt;.tap.js&lt;/code&gt; files):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forge&lt;/strong&gt; — AI observes the page (network, DOM, a11y tree) and writes a tap program&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; — Test the tap with different inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run forever&lt;/strong&gt; — The tap replays deterministically. Zero AI cost.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First run:  AI inspects → writes .tap.js     ($0.50)
Every run:  .tap.js replays deterministically ($0.00)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;8 core operations + 17 built-in operations = complete browser control protocol.&lt;/p&gt;

&lt;p&gt;A tap program is plain JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tap&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="nx"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/trending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article.Box-row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h2 a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.octicon-star&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-Runtime
&lt;/h2&gt;

&lt;p&gt;The same tap runs on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Extension&lt;/strong&gt; — Uses &lt;code&gt;chrome.scripting&lt;/code&gt; (undetectable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; — Headless capable, CI/CD friendly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt; — Native apps via Accessibility API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A new runtime implements 8 methods, gets 17 built-in operations for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community Skills
&lt;/h2&gt;

&lt;p&gt;119 community skills across 55 sites — &lt;a href="https://github.com/LeonTing1010/tap-skills" rel="noopener noreferrer"&gt;tap-skills&lt;/a&gt; (open source).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use Playwright?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;th&gt;Tap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who writes scripts?&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;AI forges them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per run&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;$0.00 (deterministic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtimes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3+ (Chrome, Playwright, macOS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Community scripts&lt;/td&gt;
&lt;td&gt;No ecosystem&lt;/td&gt;
&lt;td&gt;119 skills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://leonting1010.github.io/tap/install.sh | sh
tap github trending
tap hackernews hot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Homepage: &lt;a href="https://leonting1010.github.io/tap/" rel="noopener noreferrer"&gt;leonting1010.github.io/tap&lt;/a&gt;&lt;br&gt;
Skills (open source): &lt;a href="https://github.com/LeonTing1010/tap-skills" rel="noopener noreferrer"&gt;github.com/LeonTing1010/tap-skills&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Programs beat prompts. AI forges once, programs run forever.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Try it: &lt;a href="https://taprun.dev" rel="noopener noreferrer"&gt;taprun.dev&lt;/a&gt; | &lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Programs Beat Prompts: AI Forges Deterministic Interface Programs That Run Forever</title>
      <dc:creator>Leon</dc:creator>
      <pubDate>Wed, 01 Apr 2026 08:20:35 +0000</pubDate>
      <link>https://dev.to/leonting1010/programs-beat-prompts-ai-forges-deterministic-interface-programs-that-run-forever-j9e</link>
      <guid>https://dev.to/leonting1010/programs-beat-prompts-ai-forges-deterministic-interface-programs-that-run-forever-j9e</guid>
      <description>&lt;p&gt;Every time my AI agent automated a website interaction, it was burning tokens to solve the same problem it had already solved last run. Find the API. Locate the selector. Compose the steps. Re-solved, re-paid, every single time.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Tap&lt;/strong&gt; to fix this. The core idea: &lt;strong&gt;operating an interface is a solved problem the moment you figure out how.&lt;/strong&gt; So separate the figuring-out (AI's job, done once) from the executing (a deterministic program's job, done forever).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Paradigm: Forging
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge_inspect → forge_verify → forge_save → tap.run
    AI analyzes    AI tests      AI saves     runs forever, zero AI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI analyzes the live site, creates a &lt;code&gt;.tap.js&lt;/code&gt; file, and that file runs forever — no LLM calls, no prompts, no API keys. Runs in &amp;lt;1s. Returns structured data. Same result every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost model: ~$0.50 in tokens to forge. Then $0.00 at runtime, forever.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Protocol
&lt;/h2&gt;

&lt;p&gt;Tap defines a minimal, complete contract for interface automation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8 core operations&lt;/strong&gt; (irreducible atoms):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eval · pointer · keyboard · nav · wait · screenshot · run · capabilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;17 built-in operations&lt;/strong&gt; (composed from core):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;click · type · fill · hover · scroll · pressKey · select · upload · dialog
fetch · find · cookies · download · waitFor · waitForNetwork · ssrState · storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8 + 17 = every interaction a human can perform on any interface.&lt;/p&gt;

&lt;p&gt;A new runtime implements 8 methods → gets 17 built-ins free. Write a tap once, run it on Chrome, Playwright, macOS native apps — same protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  106 Ready-to-Use Skills
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/LeonTing1010/tap/master/install.sh | sh
tap update  &lt;span class="c"&gt;# Pulls 106 skills across 50 sites&lt;/span&gt;

&lt;span class="c"&gt;# Use&lt;/span&gt;
tap github trending &lt;span class="nt"&gt;--limit&lt;/span&gt; 5
tap hackernews hot
tap xiaohongshu hot

&lt;span class="c"&gt;# Compose with Unix pipes&lt;/span&gt;
tap github trending | tap tap/filter &lt;span class="nt"&gt;--field&lt;/span&gt; stars &lt;span class="nt"&gt;--gt&lt;/span&gt; 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skills cover GitHub, Reddit, YouTube, Hacker News, X/Twitter, Medium, arXiv, Bilibili, Zhihu, Xiaohongshu, Weibo, and 40+ more sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a .tap.js looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API-first: fetch data directly&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hackernews&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hacker News top stories&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;min_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;non_empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://hacker-news.firebaseio.com/v0/topstories.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://hacker-news.firebaseio.com/v0/item/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure JavaScript. Zero AI at runtime. The &lt;code&gt;health&lt;/code&gt; contract means &lt;code&gt;tap doctor&lt;/code&gt; can self-verify this tap automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Native
&lt;/h2&gt;

&lt;p&gt;Works with Claude Code, Cursor, Windsurf, and any MCP-compatible agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"tap"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;43 tools across 6 categories — run taps, forge new ones, inspect pages, intercept network traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;Community taps are untrusted code. Three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox&lt;/strong&gt; — Deno Worker, zero permissions (no filesystem, network, env)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static analysis&lt;/strong&gt; — 7 CI checks on every PR to tap-skills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data isolation&lt;/strong&gt; — secrets and sessions never leave your machine&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How It Compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tap&lt;/th&gt;
&lt;th&gt;Browser-Use / Stagehand&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI at runtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (forge once)&lt;/td&gt;
&lt;td&gt;Yes (every step)&lt;/td&gt;
&lt;td&gt;No (manual scripts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Detection risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Undetectable&lt;/td&gt;
&lt;td&gt;Detectable (CDP)&lt;/td&gt;
&lt;td&gt;Detectable (headless)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$0.50 once, then $0&lt;/td&gt;
&lt;td&gt;Tokens per session&lt;/td&gt;
&lt;td&gt;Free (manual)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reusable artifacts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;.tap.js (shareable)&lt;/td&gt;
&lt;td&gt;None (ephemeral)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;106 across 50 sites&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;https://github.com/LeonTing1010/tap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;106 skills, 3 runtimes, ~2,000 lines of Deno, zero dependencies.&lt;/p&gt;




&lt;p&gt;Try it: &lt;a href="https://taprun.dev" rel="noopener noreferrer"&gt;taprun.dev&lt;/a&gt; | &lt;a href="https://github.com/LeonTing1010/tap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
    </item>
  </channel>
</rss>
