<?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: tinycoder-studio</title>
    <description>The latest articles on DEV Community by tinycoder-studio (@tinycoder-studio).</description>
    <link>https://dev.to/tinycoder-studio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107174%2F3b7544d9-ac71-4445-9e36-38f5b7709a23.png</url>
      <title>DEV Community: tinycoder-studio</title>
      <link>https://dev.to/tinycoder-studio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tinycoder-studio"/>
    <language>en</language>
    <item>
      <title>I Built a Gumroad Telegram Sale Alert in 5 n8n Nodes. Zapier Charges $20/mo and Still Delays It 15 Minutes</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:04:59 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-built-a-gumroad-telegram-sale-alert-in-5-n8n-nodes-zapier-charges-20mo-and-still-delays-it-15-4e0e</link>
      <guid>https://dev.to/tinycoder-studio/i-built-a-gumroad-telegram-sale-alert-in-5-n8n-nodes-zapier-charges-20mo-and-still-delays-it-15-4e0e</guid>
      <description>&lt;p&gt;Your product sells at 2:47am. You find out at 9:15am, when you check email and the Gumroad notification is sitting three promotions down in your inbox.&lt;/p&gt;

&lt;p&gt;That's the problem nobody sells a fix for, because the fix is boring: &lt;em&gt;know the instant a sale happens.&lt;/em&gt; Not when you check the dashboard. Not when the weekly payout email lands. When it happens.&lt;/p&gt;

&lt;p&gt;Someone put it better than I can. One comment on my last post: &lt;em&gt;"silent sales you didn't even know about."&lt;/em&gt; That's the pain. Gumroad already emails you, but email waits for &lt;em&gt;you&lt;/em&gt;, and you're not a notification, you're a person with a life. Sales fire at all hours, and you're not watching.&lt;/p&gt;

&lt;p&gt;So I built the dumbest thing that solves it: a 5-node n8n workflow that pushes a Telegram alert to my phone the moment Gumroad pings. No polling. No monthly fee. Nothing else to babysit. And yeah, Zapier charges about $20/mo for a version that's still 15 minutes late on its free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built: five nodes, one line
&lt;/h2&gt;

&lt;p&gt;This is the workflow that survived my &lt;a href="https://dev.to/tinycoder-studio"&gt;7-setup n8n teardown&lt;/a&gt;, the one I called "the rare workflow that's exactly as simple as it looks." It's a straight line: webhook in, Telegram out, nothing in between that can rot.&lt;/p&gt;

&lt;p&gt;Five nodes, exact names, nothing invented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Webhook: Gumroad Sale&lt;/strong&gt; (&lt;code&gt;n8n-nodes-base.webhook&lt;/code&gt;) — Gumroad POSTs to &lt;code&gt;/webhook/gumroad-sale&lt;/code&gt; on every new sale. This is the trigger. No cron, no scheduler, no compute while idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IF: Is a Sale?&lt;/strong&gt; (&lt;code&gt;n8n-nodes-base.if&lt;/code&gt;) — passes only payloads that carry a &lt;code&gt;sale_id&lt;/code&gt;. This one node exists to kill Gumroad's test ping, which it fires the moment you save the webhook URL. Without it, you'd alert yourself on an imaginary sale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format Telegram Message&lt;/strong&gt; (&lt;code&gt;n8n-nodes-base.function&lt;/code&gt;) — builds the alert text. Paid sales get a 💰 header and a price line; free downloads get a 🎁 header. Product name, buyer email, timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram: Send Alert&lt;/strong&gt; (&lt;code&gt;n8n-nodes-base.httpRequest&lt;/code&gt;) — a plain POST to the Telegram Bot API &lt;code&gt;sendMessage&lt;/code&gt; endpoint. Token and chat ID come from env vars, never hardcoded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond&lt;/strong&gt; (&lt;code&gt;n8n-nodes-base.respondToWebhook&lt;/code&gt;) — returns HTTP 200 so Gumroad knows the ping landed and stops retrying.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole thing. Five nodes, one straight line, no community nodes, no n8n credentials to configure. The bot token lives in the request URL, pulled from &lt;code&gt;$env.TELEGRAM_BOT_TOKEN&lt;/code&gt;, which works on both n8n Cloud and self-hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Zapier math: $20/mo and still 15 minutes late
&lt;/h2&gt;

&lt;p&gt;Here's the comparison that made me build this instead of using the obvious tool.&lt;/p&gt;

&lt;p&gt;Zapier's free tier polls your Gumroad account &lt;strong&gt;every 15 minutes&lt;/strong&gt;. That means a sale at 2:47am is discovered somewhere between 2:47 and 3:02, so worst case you're a quarter hour late, on average 7.5 minutes. And the free tier ships with a &lt;strong&gt;100 tasks/month&lt;/strong&gt; ceiling. Cross it mid-month and your automation just stops running until the calendar flips. On a product that sells, that's not a tool, that's a lottery.&lt;/p&gt;

&lt;p&gt;Want it faster? Zapier's paid tier runs about &lt;strong&gt;$20/mo&lt;/strong&gt; (Professional, billed annually). What you buy is a shorter poll interval, not no interval. It's still polling. Every sale still waits for the next poll cycle to notice it. You're paying monthly, forever, for the privilege of being &lt;em&gt;less&lt;/em&gt; late.&lt;/p&gt;

&lt;p&gt;The n8n workflow doesn't poll at all by design. Gumroad's webhook is a push: the moment a sale lands, Gumroad fires a POST at my URL. No interval to miss, no recurring subscription. Self-hosted on your own box, the runtime cost is $0, and the workflow fires only when something actually happens.&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;Zapier Free&lt;/th&gt;
&lt;th&gt;Zapier Professional&lt;/th&gt;
&lt;th&gt;This n8n workflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How it finds sales&lt;/td&gt;
&lt;td&gt;Polls every 15 min&lt;/td&gt;
&lt;td&gt;Polls (shorter interval)&lt;/td&gt;
&lt;td&gt;Webhook push, no polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worst-case delay&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;td&gt;Interval-bound, still &amp;gt;0&lt;/td&gt;
&lt;td&gt;Zero by design*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task ceiling&lt;/td&gt;
&lt;td&gt;100/month&lt;/td&gt;
&lt;td&gt;Higher, still capped&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;~$20/mo, forever&lt;/td&gt;
&lt;td&gt;$0 (self-hosted)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Zero &lt;em&gt;by design&lt;/em&gt;: the webhook fires when Gumroad pings. I'm not going to sell you latency numbers this thing hasn't earned in production; the architecture just has no poll cycle in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the workflow: the JSON nobody screenshots
&lt;/h2&gt;

&lt;p&gt;Everyone posts the canvas screenshot. Nobody shows the code. Here's the actual node config from the import file, so you can judge it like a developer instead of like a customer.&lt;/p&gt;

&lt;p&gt;The filter lives in one condition on the IF node:&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="err"&gt;`json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IF: Is a Sale?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.if"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"conditions"&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;"boolean"&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="nl"&gt;"value1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.sale_id != null }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;`&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;sale_id&lt;/code&gt;, no alert. That's the entire spam filter.&lt;/p&gt;

&lt;p&gt;The formatter is where the message takes shape, and where the Gumroad payload quirks get handled:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`js&lt;br&gt;
const sale = $json;&lt;br&gt;
const product = sale.product_name || 'Unknown product';&lt;br&gt;
const email = sale.email || 'no email captured';&lt;br&gt;
const when = sale.timestamp ? new Date(sale.timestamp * 1000).toISOString() : new Date().toISOString();&lt;br&gt;
const isPaid = (sale.price || 0) &amp;gt; 0;&lt;br&gt;
const currency = sale.currency || 'USD';&lt;/p&gt;

&lt;p&gt;const lines = [&lt;br&gt;
  isPaid ? '💰 PAID SALE' : '🎁 FREE DOWNLOAD',&lt;br&gt;
  '',&lt;br&gt;
  &lt;code&gt;Product: ${product}&lt;/code&gt;,&lt;br&gt;
  &lt;code&gt;Buyer: ${email}&lt;/code&gt;,&lt;br&gt;
  &lt;code&gt;Time: ${when}&lt;/code&gt;&lt;br&gt;
];&lt;br&gt;
if (isPaid) lines.splice(3, 0, &lt;code&gt;Price: ${currency} ${(sale.price / 100).toFixed(2)}&lt;/code&gt;);&lt;/p&gt;

&lt;p&gt;return [{ json: { message: lines.join('\n'), is_paid: isPaid } }];&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Gumroad sends &lt;code&gt;price&lt;/code&gt; in &lt;strong&gt;cents&lt;/strong&gt;, which is why the code divides by 100. &lt;code&gt;timestamp&lt;/code&gt; arrives in unix seconds, hence the &lt;code&gt;* 1000&lt;/code&gt;. Both are payload quirks that would've had me staring at a wall for an hour if I'd hit them blind, so they're handled in the node instead of in your head.&lt;/p&gt;

&lt;p&gt;The send is a one-liner HTTP request reading two env vars, so your token never sits in the workflow JSON:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "name": "Telegram: Send Alert",&lt;br&gt;
  "type": "n8n-nodes-base.httpRequest",&lt;br&gt;
  "parameters": {&lt;br&gt;
    "url": "= {{ 'https://api.telegram.org/bot' + $env.TELEGRAM_BOT_TOKEN + '/sendMessage' }}",&lt;br&gt;
    "method": "POST",&lt;br&gt;
    "jsonParameters": true,&lt;br&gt;
    "bodyParametersJson": "={{ { chat_id: $env.TELEGRAM_CHAT_ID, text: $json.message, parse_mode: 'HTML' } }}"&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One more thing you get for free: free downloads alert too. &lt;code&gt;isPaid&lt;/code&gt; branches the header (💰 vs 🎁), and the price line only shows up on paid sales. Tracking free downloads in real time tells you when a launch post is actually landing, which is worth more than the paid alerts in the early days.&lt;/p&gt;

&lt;p&gt;What you'll get on your phone looks like this, straight from the formatter above:&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations (read this before importing)
&lt;/h2&gt;

&lt;p&gt;I'm not going to bury the caveats, because the caveats are the part that's actually true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structurally validated, not production-tested.&lt;/strong&gt; This workflow parses clean, the wiring is verified node-to-node, and it's version-pinned to n8n 1.x. It has &lt;strong&gt;not&lt;/strong&gt; been run against a live n8n instance or a real Gumroad sale in my environment. That's the entire honesty envelope: structurally valid and import-ready, not "battle-tested at scale." Test it before you rely on it, trigger Gumroad's test ping, or buy your own product for $0, and confirm the message lands. If that caveat bothers you, good. It should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted means &lt;em&gt;public&lt;/em&gt;.&lt;/strong&gt; Gumroad has to reach your webhook. If your n8n runs on localhost, Gumroad can't find it, and no amount of workflow cleverness fixes that. You need either n8n Cloud (the free tier works for a single workflow like this) or a self-hosted instance with a public URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workflow ships inactive.&lt;/strong&gt; The import file has &lt;code&gt;active: false&lt;/code&gt;, because nobody should ship a workflow that starts firing at strangers the second it's imported. You flip the toggle, copy the &lt;code&gt;/webhook/gumroad-sale&lt;/code&gt; URL, and paste it into Gumroad → Settings → Advanced → Webhooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's fire-and-forget.&lt;/strong&gt; No retry queue, no database, no dedupe beyond the &lt;code&gt;sale_id&lt;/code&gt; filter. If Telegram is down, the alert is gone. For a "I want to know a sale happened" push, that's the right trade. For a financial system, it isn't, but then again, neither is Zapier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it, it's free
&lt;/h2&gt;

&lt;p&gt;This workflow is a free download: import-ready JSON, MIT licensed, no catch beyond an email address so I can send you the setup doc (5 minutes, BotFather → bot token, env vars, import, activate, paste webhook URL).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/n8n-gumroad-telegram" rel="noopener noreferrer"&gt;Download the Gumroad Sale → Telegram Alert workflow →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm also building a full pack around the workflows that survived the teardown, tested-by-inspection n8n workflows plus AI agent prompt skills, &lt;a href="https://tinycoderstudio.gumroad.com/l/7-agent-skills-pack" rel="noopener noreferrer"&gt;$29 on Gumroad&lt;/a&gt;. The free workflow is the on-ramp: if a 5-node sale alert is your kind of boring, the pack is the rest of it.&lt;/p&gt;

&lt;p&gt;Your product is going to sell at 2:47am someday. The only question is whether you find out at 2:47am, or at brunch.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>gumroad</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I Tested 7 n8n Agent Setups So You Don't Have To. Here's What Actually Survived</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Sun, 20 Sep 2026 14:10:44 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-tested-7-n8n-agent-setups-so-you-dont-have-to-heres-what-actually-survived-2hdo</link>
      <guid>https://dev.to/tinycoder-studio/i-tested-7-n8n-agent-setups-so-you-dont-have-to-heres-what-actually-survived-2hdo</guid>
      <description>&lt;h1&gt;
  
  
  I Tested 7 n8n Agent Setups So You Don't Have To. Here's What Actually Survived
&lt;/h1&gt;

&lt;p&gt;n8n is where "AI agent" marketing goes to die.&lt;/p&gt;

&lt;p&gt;Every week there's another post. Beautiful canvas screenshot, nine nodes, "agentic workflows that run your business while you sleep." The comments fill up with the same question — &lt;em&gt;does it actually work?&lt;/em&gt; — and nobody answers.&lt;/p&gt;

&lt;p&gt;So I did the annoying thing. Built seven n8n agent setups, ran each through a structured validation pass, documented what it did, kept the ones that survived. No revenue screenshots. No "make $X/month" nonsense. Just what holds up and what doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "tested" means here (read this before you judge)
&lt;/h2&gt;

&lt;p&gt;Most "tested" claims on the internet mean "I ran it twice and took a screenshot." I want to be more precise than that.&lt;/p&gt;

&lt;p&gt;Here's exactly what I did with each setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validated the JSON structure — it parses clean, no malformed nodes&lt;/li&gt;
&lt;li&gt;Verified the node wiring — every connection points at a real node, no dangling references&lt;/li&gt;
&lt;li&gt;Version-pinned each workflow to n8n 1.x so it doesn't silently break on the next release&lt;/li&gt;
&lt;li&gt;Reviewed the design against the failure modes that kill agent workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I did &lt;strong&gt;not&lt;/strong&gt; do: run any of these in a live n8n instance, execute them against real services, or load-test them. No 10,000-requests-a-day claim, no "battle-tested at scale." Structurally validated and design-reviewed, not runtime-tested. If that's not enough for you, good. You should read everything on the internet the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7 setups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Gumroad sale → Telegram alert — &lt;strong&gt;SURVIVED&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A webhook trigger on Gumroad's sale event, a filter, a formatter, a Telegram message, and a response node. That's it. Five nodes. When a sale lands I get a push notification with product name, price, and buyer email.&lt;/p&gt;

&lt;p&gt;Why it survived: it's the rare workflow that's exactly as simple as it looks. Trigger-driven (fires only when something happens, burns zero compute sitting idle), single-purpose, deterministic output. Every other workflow on this list gets measured against it. Details below, and it's yours for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Webhook → route task to specialist agent — &lt;strong&gt;SURVIVED (with a caveat)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The multi-agent router: one webhook in, a keyword/pattern router, task dispatched to the right specialist. Works, as long as your routing rules are a fixed schema and not vibes. The moment I tried "let the LLM decide the route," outputs got clever in the bad way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The caveat:&lt;/strong&gt; a router is only as good as the contract between agents. Define the input schema first, then the routes. No schema, no survival.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Webhook → security scan → Slack alert — &lt;strong&gt;SURVIVED (as a linter, not a scanner)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Feeding code through a vulnerability-pattern scanner and alerting on hits. As a quick trigger-driven check on pasted snippets, genuinely useful. As a security product, it isn't. It's a regex linter, and pattern lists rot fast. Treat it as a first pass that catches the obvious stuff (hardcoded secrets, eval, SQLi patterns) and you're fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Webhook → humanize text → metrics — &lt;strong&gt;SURVIVED (as a formatting pass)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The honest take: "humanization" via phrase replacement is a formatting pass, not magic. It kills the most obvious AI tells (&lt;em&gt;"In today's digital landscape"&lt;/em&gt;, &lt;em&gt;"it is important to note"&lt;/em&gt;) and reports the before/after. It survived because it does one thing and reports what it did. It did &lt;em&gt;not&lt;/em&gt; survive as a rewrite engine. LLM-generated rewrites drift, and you pay per call.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Webhook → analyze content → SEO report — &lt;strong&gt;SURVIVED (but it's commodity)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sentiment, keyword extraction, reading time, a quick SEO readout. Useful, deterministic, survived the pass. But a dozen other tools do this. It's a nice-to-have in a pack, not a reason to buy one.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Pay-per-call x402 API server — &lt;strong&gt;BROKE&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This one tried to be a product inside a workflow: crypto wallet, USDC payments, Cloudflare tunnel, payment verification, rate limits. The design review found a wall of surface area that murders the "5 minute setup" promise. Every dependency (wallet setup, facilitator verification, tunnel flakiness) is a new way to fail. The revenue table everyone quotes in these posts is fantasy without distribution. Nobody calls an API nobody knows exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it broke:&lt;/strong&gt; it monetized before it validated. Get the workflow working for one human first. Bolt on payments later, if ever.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Scheduled "agent digest" — cron scrape → summarize → send — &lt;strong&gt;BROKE&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The classic: every morning, scrape a few sources, summarize, send to Telegram. The design kills it: scheduled, non-deterministic, silent failure. Sources change their HTML structure, feeds go stale, and an AI summarizer will happily summarize garbage with total confidence. Nobody watches a cron job, so nothing fails loudly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it broke:&lt;/strong&gt; not trigger-driven, output not deterministic, failure wasn't loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the survivors have in common
&lt;/h2&gt;

&lt;p&gt;Seven setups, two patterns. The five survivors share three traits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger-driven, not scheduled.&lt;/strong&gt; They fire when something &lt;em&gt;happens&lt;/em&gt; (a webhook, a sale, a manual test). Nobody watches them, so nothing rots silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single purpose, deterministic output.&lt;/strong&gt; JSON in, formatted message out. The dumber the contract, the longer the workflow lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure is loud.&lt;/strong&gt; Error paths are actual paths, not afterthoughts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The two that broke tried to be &lt;em&gt;products&lt;/em&gt;: payments, scheduling, open-ended generation. That's the line, and it's sharp.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one worth stealing (free, no catch beyond an email)
&lt;/h2&gt;

&lt;p&gt;Every digital creator needs this one first: knowing a sale happened in real time. Not a dashboard you check, a push notification that lands while it's still happening.&lt;/p&gt;

&lt;p&gt;The workflow that survived the cleanest is the &lt;a href="https://tinycoderstudio.gumroad.com/l/n8n-gumroad-telegram" rel="noopener noreferrer"&gt;&lt;strong&gt;Gumroad Sale → Telegram Alert&lt;/strong&gt;&lt;/a&gt;. Import-ready JSON, MIT licensed, free download. Five nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gumroad Webhook&lt;/strong&gt; — Gumroad pings it on every new sale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt; — only real sales, skip test events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formatter&lt;/strong&gt; — builds the alert message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram&lt;/strong&gt; — sends the alert to your chat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond&lt;/strong&gt; — answers Gumroad so it stops retrying&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key node, so you can see it's not a screenshot of a dream:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Telegram: Send Alert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.httpRequest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;850&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"= {{ 'https://api.telegram.org/bot' + $env.TELEGRAM_BOT_TOKEN + '/sendMessage' }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsonParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bodyParametersJson"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ { chat_id: $env.TELEGRAM_CHAT_ID, text: $json.message, parse_mode: 'HTML' } }}"&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;Setup is about five minutes: import the JSON, set two environment variables (your Telegram bot token and chat id), plug in your Gumroad webhook URL (Gumroad settings → Ping → point it at the webhook node), done. Works on n8n cloud or self-hosted, nothing else to install.&lt;/p&gt;

&lt;p&gt;One honest gotcha so you don't lose twenty minutes like I did: Gumroad sends &lt;strong&gt;test notifications&lt;/strong&gt; when you first save the Ping URL. Trust the filter node. It's there specifically so you don't alert yourself on an imaginary sale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/n8n-gumroad-telegram" rel="noopener noreferrer"&gt;Download the free workflow →&lt;/a&gt;&lt;/strong&gt; (Free, email required — you'll get the setup doc plus updates when I add more workflows.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern, not the pack
&lt;/h2&gt;

&lt;p&gt;If you take one thing from this: the workflows that survive are the boring ones. Trigger-driven, single-purpose, loud failures. Every "agent empire" post you see this week is selling you setup #6 or #7. The ones that broke.&lt;/p&gt;

&lt;p&gt;I'm building a pack around the ones that survived, currently &lt;strong&gt;5 skills + 4 n8n workflows&lt;/strong&gt;, structured the same way each was validated here, expanding to 15–20 tested workflows. If that sounds like your kind of boring: the free workflow above is the on-ramp, and the full pack is &lt;a href="https://tinycoderstudio.gumroad.com/l/7-agent-skills-pack" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want the whole validation, not just the survivor.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>n8n</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Built a Headless Browser Toolkit With Chrome DevTools Protocol</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Thu, 17 Sep 2026 13:47:28 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-built-a-headless-browser-toolkit-with-chrome-devtools-protocol-58m</link>
      <guid>https://dev.to/tinycoder-studio/i-built-a-headless-browser-toolkit-with-chrome-devtools-protocol-58m</guid>
      <description>&lt;h2&gt;
  
  
  I Built a Headless Browser Toolkit With Chrome DevTools Protocol — Here Is the Full Architecture
&lt;/h2&gt;

&lt;p&gt;I needed to post to Twitter, Reddit, and dev.to from one codebase. Twitter's API has rate limits. Reddit bans bots. dev.to is fine. So I wrote 54 lines of JavaScript that automates the whole thing — no API keys, no OAuth, just a headless Chrome doing what you'd do manually.&lt;/p&gt;

&lt;p&gt;It's the browser automation equivalent of raw SQL. You skip all the abstraction layers and go straight to what actually works.&lt;/p&gt;

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

&lt;p&gt;Every platform says "use our API." So you do. Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Twitter&lt;/strong&gt; API tiers are restrictive and expensive for indie devs. Free tiers barely cover casual use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reddit&lt;/strong&gt; moderation is aggressive toward automation — accounts that post programmatically without human review get flagged quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gumroad's&lt;/strong&gt; API covers product management, but it's limited compared to what you can do through their web interface. For complex product setups with multiple files and variants, browser automation is sometimes faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; kills APIs every 18 months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, the actual browser sitting on your machine can do everything a logged-in user can do. That's the gap I went after.&lt;/p&gt;

&lt;p&gt;Puppeteer is maintained by the Chrome team and works fine, but the CDP docs are scattered across Chrome DevTools protocol pages that haven't been updated since 2020. Selenium is ancient — WebDriver is a different paradigm entirely. Cypress is for testing, not automation.&lt;/p&gt;

&lt;p&gt;I wanted something that did one thing well: give me the browser so I can control it programmatically. 54 lines later, I had it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution — cdpp.js
&lt;/h2&gt;

&lt;p&gt;Here's the entire wrapper:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;browser&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;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;page&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&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;url&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines to launch a browser. Playwright handles Chromium, Firefox, and WebKit — I only use Chromium. No config needed. No &lt;code&gt;playwright.config.js&lt;/code&gt;. No test runners.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pageText&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigation and content reading. &lt;code&gt;pageText()&lt;/code&gt; pulls all visible text from the page — useful for scraping, link checking, or verifying content posted correctly.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clickByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`text="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Instead of brittle CSS selectors or XPath queries, I click by what the user sees. &lt;code&gt;clickByText("Tweet")&lt;/code&gt; finds the first visible element containing that text and clicks it. Playwright's auto-wait handles the timing — no &lt;code&gt;setTimeout&lt;/code&gt; hacks.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fillInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setFileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filePath&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filePath&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;Form filling and file upload. &lt;code&gt;fillInput&lt;/code&gt; clears the field first, then types. &lt;code&gt;setFileInput&lt;/code&gt; handles the &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; element that's invisible to normal click automation.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evalInTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;js&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;js&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;Screenshots for debugging or documentation. &lt;code&gt;evalInTab&lt;/code&gt; runs arbitrary JavaScript inside the page context — the same as opening DevTools console and typing code.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;disconnect&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;Cleanup. Releases the Chromium process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;54 lines total.&lt;/strong&gt; Just functions that call Playwright.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Example: Posting a Tweet
&lt;/h2&gt;

&lt;p&gt;Here's how &lt;code&gt;twitter-poster.js&lt;/code&gt; uses the toolkit:&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;cdp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./cdpp&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postTweet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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://x.com/compose/post&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="tweetTextarea_0"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clickByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tweet&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tweet-posted.png&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&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;That's the core. Navigate to the compose screen, fill the text area, click Tweet, screenshot for confirmation.&lt;/p&gt;

&lt;p&gt;No API key. No OAuth. No rate limits. It works because the browser is already logged in — the session cookies are in the Chrome profile. Same way you'd post a tweet manually.&lt;/p&gt;

&lt;p&gt;The real version handles login state detection, waits for the compose modal to fully load, and retries if Twitter's React app hasn't rendered the textarea yet. But the core flow is exactly this.&lt;/p&gt;

&lt;p&gt;The API tiers make automation painful. CDP sidesteps those limits — you post as fast as the page loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Example: Filling a Gumroad Form
&lt;/h2&gt;

&lt;p&gt;Okay but real talk — Gumroad's API covers product management, but it's limited for complex setups. Their internal endpoints require CSRF tokens that rotate. So I automate the form directly.&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;await&lt;/span&gt; &lt;span class="nx"&gt;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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://app.gumroad.com/products/new&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product_name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My New Product&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product_description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product description here&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product_price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;29&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type="file"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./cover.png&lt;/span&gt;&lt;span class="dl"&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;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clickByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Publish&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;p&gt;This fills 15+ fields — name, description, price, permalink, cover image, content files. The file upload uses &lt;code&gt;setFileInput&lt;/code&gt; which handles the hidden &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; that Gumroad uses.&lt;/p&gt;

&lt;p&gt;The full script processes a product definition from a JSON file and fills every field automatically. Before this, creating a product on Gumroad took 20 minutes of manual clicking. Now it takes 30 seconds.&lt;/p&gt;

&lt;p&gt;This is how CDP works in practice: you use the exact same interface the human uses, sidestepping missing or rate-limited APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contrast: CDP vs REST API
&lt;/h2&gt;

&lt;p&gt;Look, I know what you're thinking — isn't this just reinventing the wheel? Not really. Here's the tradeoff, concrete:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dev.to posting — REST API (129 lines):&lt;/strong&gt;&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;response&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://dev.to/api/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webdev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;javascript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Well-documented. Rate-limited to 30 articles/30 seconds, which nobody hits. The API handles markdown rendering, tag validation, canonical URLs — everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Twitter posting — CDP (200+ lines):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate. Wait for React to render. Find the textarea by data-testid attribute. Type content character by character (because React synthetic events don't trigger on &lt;code&gt;page.fill&lt;/code&gt;). Click Tweet. Handle the confirmation modal. Screenshot.&lt;/p&gt;

&lt;p&gt;When to use which:&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;&lt;strong&gt;CDP&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;REST API&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slow (full page load)&lt;/td&gt;
&lt;td&gt;Fast (single HTTP call)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Breaks when UI changes&lt;/td&gt;
&lt;td&gt;Stable with versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser session (cookies)&lt;/td&gt;
&lt;td&gt;API key / OAuth token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limits&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser speed only&lt;/td&gt;
&lt;td&gt;Platform-defined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Capabilities&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anything a user can do&lt;/td&gt;
&lt;td&gt;Only what the API exposes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UI changes = your code breaks&lt;/td&gt;
&lt;td&gt;API changes = you update&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;My rule:&lt;/strong&gt; If the platform has a good API, use it. dev.to, GitHub, Slack — all have clean REST APIs. Use them.&lt;/p&gt;

&lt;p&gt;If the platform's API is rate-limited, expensive, or missing features — use CDP. Twitter, Reddit, Gumroad, LinkedIn. The browser doesn't care about your API tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should I Use CDP vs REST API?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use CDP when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform has no API or a severely limited one&lt;/li&gt;
&lt;li&gt;You need to perform actions that the API doesn't expose&lt;/li&gt;
&lt;li&gt;API rate limits are blocking your automation&lt;/li&gt;
&lt;li&gt;You need to use a logged-in session (no API key available)&lt;/li&gt;
&lt;li&gt;The platform actively discourages automation (Reddit, LinkedIn)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use REST API when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform has a well-documented, stable API&lt;/li&gt;
&lt;li&gt;You need speed (CDP is 10-100x slower per request)&lt;/li&gt;
&lt;li&gt;You're building something that needs to run at scale&lt;/li&gt;
&lt;li&gt;The API covers everything you need&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use both when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want reliability for the core flow (API) but need to handle edge cases (CDP)&lt;/li&gt;
&lt;li&gt;You're migrating from one approach to the other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The toolkit supports both patterns. &lt;code&gt;devto-poster.js&lt;/code&gt; uses the REST API. &lt;code&gt;twitter-poster.js&lt;/code&gt; uses CDP. Same codebase, same patterns, different transport layer.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Puppeteer is fine.&lt;/strong&gt; It works. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Playwright has better auto-wait.&lt;/strong&gt; Puppeteer requires manual &lt;code&gt;waitForSelector&lt;/code&gt; calls. Playwright waits for elements to be actionable before clicking. Fewer race conditions, less debugging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Playwright supports multiple browsers natively.&lt;/strong&gt; If you ever need to test in Firefox or WebKit, you're one line away. Puppeteer is Chromium-only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Playwright's &lt;code&gt;text=&lt;/code&gt; selector is magic.&lt;/strong&gt; &lt;code&gt;page.click('text=Submit')&lt;/code&gt; finds visible text. Puppeteer requires you to write your own XPath or use &lt;code&gt;page.evaluate&lt;/code&gt; to find elements by content.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Playwright has faster feature velocity and broader browser support.&lt;/strong&gt; Puppeteer remains maintained by the Chrome team with a CDP-first focus, but Playwright's API is more developer-friendly for automation tasks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already have a Puppeteer codebase, don't rewrite it. But for new projects, Playwright is the better default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdpp.js (54 lines)
├── connect() → Playwright chromium.launch()
├── Navigation: goto(), pageText()
├── Interaction: clickByText(), fillInput(), setFileInput()
├── Observation: screenshot(), evalInTab(), listTargets()
└── Cleanup: disconnect()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything builds on top of this. &lt;code&gt;twitter-poster.js&lt;/code&gt; imports cdpp. &lt;code&gt;reddit-poster.js&lt;/code&gt; imports cdpp. &lt;code&gt;verify-links.js&lt;/code&gt; imports cdpp. &lt;code&gt;gumroad-upload.js&lt;/code&gt; imports cdpp.&lt;/p&gt;

&lt;p&gt;One wrapper. Multiple tools. No shared state beyond the browser instance.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;listTargets()&lt;/code&gt; function is worth mentioning — it enumerates all open tabs and frames. Useful when you need to interact with popups or iframes without knowing their selector in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality Check
&lt;/h2&gt;

&lt;p&gt;Browser automation &lt;strong&gt;violates the Terms of Service&lt;/strong&gt; of most platforms. Account termination is a real risk. Use at your own risk.&lt;/p&gt;

&lt;p&gt;Anti-bot systems (Cloudflare, DataDome, behavioral analysis) &lt;strong&gt;detect and block headless Chrome&lt;/strong&gt;. Canvas fingerprinting, &lt;code&gt;navigator.webdriver&lt;/code&gt; detection, and timing analysis are real. You'll hit walls on sites that invest in bot protection.&lt;/p&gt;

&lt;p&gt;The 54 lines are the wrapper. Production scripts like &lt;code&gt;twitter-poster.js&lt;/code&gt; are 200+ lines with retry logic, error handling, and session management. Don't expect to copy-paste the wrapper and have it work on every site.&lt;/p&gt;

&lt;p&gt;CDP doesn't scale — one browser instance = one session. For scale, consider cloud browser services like Browserless or Browserbase.&lt;/p&gt;

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

&lt;p&gt;Playwright + a thin CDP wrapper worked for me. Your mileage may vary.&lt;/p&gt;

&lt;p&gt;If a platform's API is rate-limited, expensive, or missing features, browser automation is a viable fallback. The browser is the most universal interface we've built — every platform has a web version, and CDP lets you control it programmatically.&lt;/p&gt;

&lt;p&gt;For new projects, start with the platform's API if one exists. When it doesn't — or when it's too restrictive — a small wrapper around Playwright gets the job done.&lt;/p&gt;

&lt;p&gt;Found this useful? I'm building a PWA dev toolbox — check it out → &lt;a href="https://toolbox-lilac-three.vercel.app" rel="noopener noreferrer"&gt;TinyCoder Web Toolbox&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How Independent Insurance Agencies Lose $50K+ Per Year in Missed Leads (And How to Fix It)</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Thu, 17 Sep 2026 03:33:16 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/how-independent-insurance-agencies-lose-50k-per-year-in-missed-leads-and-how-to-fix-it-49ka</link>
      <guid>https://dev.to/tinycoder-studio/how-independent-insurance-agencies-lose-50k-per-year-in-missed-leads-and-how-to-fix-it-49ka</guid>
      <description>&lt;p&gt;&lt;a href="https://insurance-digital-employee.vercel.app" rel="noopener noreferrer"&gt;Free Audit Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How Independent Insurance Agencies Lose $50K+ Per Year in Missed Leads (And How to Fix It)&lt;/p&gt;

&lt;p&gt;You run an independent agency. You have 5, 10, maybe 15 staff. Your book of business sits somewhere between $1M and $10M in annual premium. You're not a tech company — you're a people company that happens to sell insurance.&lt;/p&gt;

&lt;p&gt;And somewhere between the quote request that lands on your website at 9:47 PM and the phone call your team makes at noon the next day, you're losing money you'll never see again.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical. It's a line item on your P&amp;amp;L that doesn't show up as a line item — because you never had the revenue to begin with.&lt;/p&gt;

&lt;p&gt;Let's talk about the real dollar cost of slow response, what the data actually says, and what you can do about it without hiring a tech team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math: What a Missed Lead Actually Costs You
&lt;/h2&gt;

&lt;p&gt;Start with the basics. The average independent insurance agency takes &lt;strong&gt;47 hours or more&lt;/strong&gt; to respond to a new lead, according to Onyx CRM. Not 47 minutes. Forty-seven hours.&lt;/p&gt;

&lt;p&gt;Here's what that means in dollar terms.&lt;/p&gt;

&lt;p&gt;Let's say your agency pays for internet leads — from a provider, from your website, from a referral partner. Each lead costs you somewhere between $25 and $75, depending on the source. A fair middle estimate for most independent agencies is &lt;strong&gt;$40 per lead&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're buying or generating 100 leads per month — again, a reasonable number for a mid-size independent — that's $4,000 per month in lead cost. $48,000 per year.&lt;/p&gt;

&lt;p&gt;Now let's talk about what happens to those leads.&lt;/p&gt;

&lt;p&gt;Only about &lt;strong&gt;7% of agencies respond to leads within five minutes&lt;/strong&gt;, according to Velocify. That's not a misprint. Seven percent. Which means 93% of your competitors are playing the same game you are — taking hours, sometimes days, to get back to a prospect who filled out a form or called in after hours.&lt;/p&gt;

&lt;p&gt;Meanwhile, the research from InsideSales.com is unequivocal: agencies that contact leads within five minutes are &lt;strong&gt;21 times more likely to qualify them&lt;/strong&gt; compared to those who wait 30 minutes. And when MIT and InsideSales.com measured follow-up timing against actual conversation outcomes, agents who followed up within one hour were &lt;strong&gt;seven times more likely&lt;/strong&gt; to have a meaningful conversation than those who waited even two hours.&lt;/p&gt;

&lt;p&gt;Read that again. The window between one hour and two hours is where deals go to die.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the Numbers
&lt;/h3&gt;

&lt;p&gt;If you generate 100 leads per month and your current speed-to-lead puts you in the 47-hour average, you're converting somewhere around 2-3% of those leads into policies. That's 2-3 new policies per month on a 100-lead spend.&lt;/p&gt;

&lt;p&gt;If you moved your average response time under five minutes, and the 21x qualification multiplier holds even conservatively — say you only capture a fraction of that uplift — you're looking at 6-8 policies per month instead of 2-3. That's &lt;strong&gt;3-5 additional policies per month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At an average annual premium of $2,000 per policy, with an average commission of 12%, each additional policy is worth roughly &lt;strong&gt;$240 in first-year commission&lt;/strong&gt;. Three to five extra policies per month is $720 to $1,200 in additional monthly revenue. Over a year, that's &lt;strong&gt;$8,640 to $14,400&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that's just the first-year commission. What about renewals? What about the referral that comes from a client who had a great onboarding experience? What about the umbrella policy, the auto add-on, the commercial line they mention to their business partner?&lt;/p&gt;

&lt;p&gt;Conservative estimate: a single well-handled lead is worth $500-$1,500 in lifetime revenue to your agency. When you're ignoring or under-following 95% of your leads, you're not leaving $50K on the table. You're leaving significantly more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Agencies Don't Realize They're Losing
&lt;/h2&gt;

&lt;p&gt;The raw dollar loss is painful enough. But the hidden cost is worse.&lt;/p&gt;

&lt;p&gt;Here's what 93% of agencies never do with a lead: &lt;strong&gt;call back a second time&lt;/strong&gt;. According to Agency Performance Partners, 93% of insurance leads are never called back even once after the initial attempt. Not twice. Once.&lt;/p&gt;

&lt;p&gt;Leads360 dug deeper and found that &lt;strong&gt;52% of insurance agents claim they never follow up with internet leads at all&lt;/strong&gt;. Zero. The lead comes in, nobody picks up the phone, and that's the end of it. Another 44% stop after just one follow-up attempt. And across the industry, the average agency makes only &lt;strong&gt;1.3 call attempts&lt;/strong&gt; on internet leads.&lt;/p&gt;

&lt;p&gt;One point three.&lt;/p&gt;

&lt;p&gt;Think about what a prospect experiences. They visit your website at 10 PM on a Tuesday because their homeowners policy renewal is coming up and they want to shop rates. They fill out a form. They go to bed. The next morning, nothing. No call. No email. Maybe a generic auto-reply that says "we received your inquiry and will be in touch."&lt;/p&gt;

&lt;p&gt;By noon, they've already called another agency. By Wednesday, they've bound a policy. And your agency — the one that paid for that lead — gets nothing.&lt;/p&gt;

&lt;p&gt;That's not just a lost sale. That's a lost relationship. A lost referral chain. A lost online review. A lost opportunity for the prospect to tell three friends about how responsive and professional you are.&lt;/p&gt;

&lt;p&gt;And the compounding effect is brutal. In a market where &lt;strong&gt;78% of customers go with the first agent to respond&lt;/strong&gt;, according to Insurance News Net, you're not just losing that one policy. You're training yourself to believe that internet leads don't work. You'll tell other agents that leads are "low quality." You'll reduce your spend or stop generating them altogether. Meanwhile, the agency down the street that invested in a five-minute response system is collecting the clients you paid to acquire.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Good" Looks Like: Speed-to-Lead Benchmarks
&lt;/h2&gt;

&lt;p&gt;Let's reset expectations. Here's what fast response actually looks like for an independent agency:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Gold Standard: Under 5 Minutes
&lt;/h3&gt;

&lt;p&gt;Only 22.6% of agencies use technology to reach out to leads within the first minute, according to Leads360. That's your competition. If you can consistently respond to inbound leads within five minutes — whether that's a phone call, a text, or a personalized email — you're in the top tier of the industry. Not top 20%. Top 5%.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Minimum Viable Response: Under 1 Hour
&lt;/h3&gt;

&lt;p&gt;MIT's research is clear: the one-hour mark is the cliff. Below one hour, you're seven times more likely to have a real conversation. Above two hours, you're in freefall. If you can't hit five minutes, hit one hour. It's still dramatically better than the 47-hour industry average.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Follow-Up Standard: 5-7 Touches
&lt;/h3&gt;

&lt;p&gt;The single call attempt that 44% of agents stop after? It's not enough. Industry data consistently shows that it takes 5-7 follow-up attempts to reach a lead who hasn't responded. Most prospects aren't ignoring you — they're busy, distracted, or saw your message at a bad time. Each touchpoint is a coin flip. You need enough flips.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cadence: Day 0, Day 1, Day 3, Day 7, Day 14, Day 21, Day 30
&lt;/h3&gt;

&lt;p&gt;That's a simple, repeatable follow-up sequence. Initial contact on Day 0. Follow-up the next day. Then spread the remaining touches over a month. Not aggressive. Not spammy. Just persistent enough to catch someone when they're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Practical Steps You Can Implement This Week
&lt;/h2&gt;

&lt;p&gt;You don't need a massive technology overhaul. You need a system. Here's how to build one without disrupting your operation:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Audit Your Current Speed
&lt;/h3&gt;

&lt;p&gt;Before you fix anything, measure it. Pull up your CRM or lead source and look at the last 50 inbound leads. How many did you call within five minutes? Within one hour? Within 24 hours? The number will embarrass you — and that's the point. You need a baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Route Leads Immediately
&lt;/h3&gt;

&lt;p&gt;The biggest bottleneck isn't willingness — it's routing. When a lead comes in through your website, it sits in an inbox or a form submission queue. By the time someone checks it, hours have passed. Set up automatic alerts that push lead information directly to a mobile device the instant it arrives. Email won't cut it. You need a text, a push notification, or a direct CRM ping.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create a First-Call Script for Speed Leads
&lt;/h3&gt;

&lt;p&gt;When you're calling someone within five minutes, you need a script optimized for speed, not a 15-minute consult. The goal is simple: confirm they're still shopping, get a time for a real conversation, and set the next touchpoint. That's it. Don't try to close on the first call. Try to start the relationship.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Build a Follow-Up Cadence and Stick to It
&lt;/h3&gt;

&lt;p&gt;Use the Day 0, Day 1, Day 3, Day 7, Day 14, Day 21, Day 30 structure as your baseline. Automate what you can — text reminders, email sequences — but make sure a human touches each lead at least twice in the first 48 hours. The combination of automated persistence and personal outreach is what moves the needle.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Assign Ownership
&lt;/h3&gt;

&lt;p&gt;Every lead needs a name attached to it within 60 seconds of arrival. Not a team. A person. When leads go to "the agency," nobody owns them. When they go to Sarah, Sarah calls within five minutes. Accountability is the simplest technology you can deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Review Weekly
&lt;/h3&gt;

&lt;p&gt;Every Friday, pull up the week's leads. How many came in? How many were contacted within five minutes? How many within an hour? How many received follow-up attempts beyond the first call? Reviewing this data takes 15 minutes and will transform your conversion rate faster than any software purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compounding Effect of Getting This Right
&lt;/h2&gt;

&lt;p&gt;Speed-to-lead isn't a tactic. It's a system that compounds.&lt;/p&gt;

&lt;p&gt;Faster response means more conversations. More conversations mean more quotes. More quotes mean more policies. More policies mean more referrals. More referrals mean lower customer acquisition cost. Lower CAC means more budget for growth. It's a flywheel, and the spark is the first five minutes.&lt;/p&gt;

&lt;p&gt;Most independent agencies will read this article and do nothing. The 52% who never follow up with internet leads aren't going to suddenly become responsive organizations because of a blog post.&lt;/p&gt;

&lt;p&gt;But you're not most agencies. You're reading this because you already sense that the gap between where your agency is and where it could be is measured in tens of thousands of dollars per year. And you're right.&lt;/p&gt;

&lt;p&gt;The question isn't whether slow response costs you money. The data is overwhelming. The question is whether you're going to fix it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're an independent insurance agency and want to know your actual speed-to-lead numbers, I'm running free audits this month. No pitch, no obligation — just data. &lt;a href="https://insurance-digital-employee.vercel.app" rel="noopener noreferrer"&gt;Get your free audit →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Onyx CRM — Average response time data&lt;/li&gt;
&lt;li&gt;Velocify — 5-minute response conversion rates (15M+ leads)&lt;/li&gt;
&lt;li&gt;InsideSales.com / MIT — Follow-up timing vs. conversation outcomes&lt;/li&gt;
&lt;li&gt;Agency Performance Partners — Lead follow-up statistics&lt;/li&gt;
&lt;li&gt;Leads360 — Agency follow-up behavior analysis&lt;/li&gt;
&lt;li&gt;Insurance News Net — First-responder market share data&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>insurance</category>
      <category>productivity</category>
      <category>data</category>
      <category>saas</category>
    </item>
    <item>
      <title>I Built a 7-Agent System That Earns USDC Per Call — Here Are the 5 Skills That Matter Most</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Mon, 14 Sep 2026 22:23:23 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-built-a-7-agent-system-that-earns-usdc-per-call-here-are-the-5-skills-that-matter-most-38e2</link>
      <guid>https://dev.to/tinycoder-studio/i-built-a-7-agent-system-that-earns-usdc-per-call-here-are-the-5-skills-that-matter-most-38e2</guid>
      <description>&lt;p&gt;&lt;em&gt;Update 2026-09-15:&lt;/em&gt;* Machine reboot rotated our demo URL. New live endpoint: &lt;code&gt;https://pose-textbooks-heroes-configure.trycloudflare.com&lt;/code&gt;. Bookmark the status page for the current URL — tunnel endpoints rotate. &lt;a href="https://status-page-deploy.vercel.app/" rel="noopener noreferrer"&gt;https://status-page-deploy.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been running a 7-agent AI system for weeks. It routes tasks, scans code for vulnerabilities, humanizes text, analyzes content — and now it earns USDC per API call via x402.&lt;/p&gt;

&lt;p&gt;Here's what I built, what works, and the 5 skills you can steal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The System
&lt;/h2&gt;

&lt;p&gt;Seven specialist agents, each with a job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;oracle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supreme orchestrator — routes tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;admin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Security, infrastructure, hardening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dev-worker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code, builds, deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;loki&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Marketing, strategy, growth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;product-reviewer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compliance, content safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;navigator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser automation, scraping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;graphic-artist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Image prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;humanizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI text → natural human tone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They coordinate through a central oracle. You give it a task, it routes to the right specialist.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Money Part: x402
&lt;/h2&gt;

&lt;p&gt;x402 is an HTTP-native payment protocol. Client calls API → gets 402 Payment Required → pays USDC on Base → gets result. No Stripe, no auth, no invoicing.&lt;/p&gt;

&lt;p&gt;I deployed 4 endpoints on a $0 Cloudflare Tunnel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/oracle/route&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.001&lt;/td&gt;
&lt;td&gt;oracle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/humanize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.005&lt;/td&gt;
&lt;td&gt;humanizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/security-scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.01&lt;/td&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/analyze&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.003&lt;/td&gt;
&lt;td&gt;loki&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Live at: &lt;code&gt;https://pose-textbooks-heroes-configure.trycloudflare.com&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 Skills (Packaged for You)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Oracle Coordination — Task Routing
&lt;/h3&gt;

&lt;p&gt;Routes any task to the right agent. Keyword-based, extensible, &amp;lt;1ms.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
javascript
function routeToAgent(task) {
  const t = task.toLowerCase();
  if (/secur|malware|firewall/.test(t)) return { agent: 'admin', reason: 'Security' };
  if (/code|build|deploy/.test(t)) return { agent: 'dev-worker', reason: 'Code' };
  if (/market|strateg|growth/.test(t)) return { agent: 'loki', reason: 'Marketing' };
  // ... etc
  return { agent: 'oracle', reason: 'Orchestration' };
}
2. Security Scanning — 20+ Patterns
Catches hardcoded secrets, eval(), SQL injection, XSS, shell injection, privilege escalation.
const patterns = [
  { pattern: /password\s*[:=]\s*['"]?\w+['"]?/gi, type: 'Hardcoded password', severity: 'HIGH' },
  { pattern: /eval\s*\(/gi, type: 'Code injection (eval)', severity: 'MEDIUM' },
  { pattern: /SELECT.*FROM.*WHERE.*['"]/gi, type: 'SQL injection', severity: 'HIGH' },
  // ... 17 more
];
3. Text Humanization — 20+ Replacements
Turns "In today's digital landscape, it is important to note that we should utilize robust solutions" into "Today, Note that we should use strong solutions."
const replacements = [
  [/In today's digital landscape/gi, "Today"],
  [/it is important to note that/gi, "Note that"],
  [/utilize/gi, "use"],
  [/leverage/gi, "use"],
  [/furthermore/gi, "Also"],
  // ... 16 more
];
4. Content Analysis — SEO + Sentiment
Stats, top keywords, reading time, sentiment score.
function analyzeContent(text) {
  // Returns: { stats, top_words, sentiment_score, sentiment_label }
}
5. x402 API Builder — Deploy Your Own
250 lines, Node 16, zero deps. Deploy in 3 commands:
nohup node server.js &amp;gt; server.log 2&amp;gt;&amp;amp;1 &amp;amp;
cloudflared tunnel --url http://localhost:3000
# Copy the https://*.trycloudflare.com URL
The n8n Templates
Each skill has a ready-to-import n8n workflow:
- Webhook trigger → x402 payment → agent call → formatted response
- Slack alerts for high-severity findings
- SEO recommendations for content
- Metrics tracking for humanization
Import → Configure credentials → Deploy.
Revenue Reality
At current pricing:
- 100 calls/day across all endpoints = ~$57/month
- 1,000 calls/day = ~$570/month
Your revenue depends on distribution. The pack includes n8n templates so buyers can integrate instantly.
Get the Pack
$29 on Gumroad — Includes all 5 skills (markdown + code), 5 n8n templates (JSON), complete x402 server, docs, MIT license.
Agent Skills Pack on Gumroad (https://tinycoderstudio.gumroad.com/l/7-agent-skills-pack)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>web3</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Found an Undocumented MCP Server on OpenSea — and It Leaked Usernames for Any Wallet</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Sun, 13 Sep 2026 03:28:36 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-43om</link>
      <guid>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-43om</guid>
      <description>&lt;h1&gt;
  
  
  I Found an Undocumented MCP Server on OpenSea — and It Leaked Usernames for Any Wallet
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; OpenSea runs an undocumented MCP server at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;. One of its tools hands out API keys to anyone who asks, and another lets you resolve any Ethereum address &lt;em&gt;with an OpenSea profile&lt;/em&gt; to its owner's username. Reported to Bugcrowd, now sharing the story and a free recon kit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was poking around OpenSea's tooling one afternoon — just trying to understand how some of their internal features worked. Nothing fancy, no scanners, no brute force. Just curiosity and a browser.&lt;/p&gt;

&lt;p&gt;That's when I stumbled onto something that wasn't meant for public eyes: an undocumented MCP server sitting at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first glance it looked like just another internal API. But one particular tool caught my attention: &lt;code&gt;get_instant_api_key&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;Turns out, that little endpoint was an unintended exposure of something far more valuable. By sending a straightforward POST request to &lt;code&gt;https://api.opensea.io/api/v2/auth/keys&lt;/code&gt;, I could pull a valid API key straight out of the server. No authentication hoops — just a direct line to a credential that should have stayed hidden.&lt;/p&gt;

&lt;p&gt;With that key in hand, I called the &lt;code&gt;get_profile&lt;/code&gt; tool and, suddenly, I could look up a username for any Ethereum address with an OpenSea profile. I tested it on my own wallet, then on a well-known one. Both resolved instantly.&lt;/p&gt;

&lt;p&gt;It felt like finding a master key to a building you weren't supposed to enter.&lt;/p&gt;

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

&lt;p&gt;Information disclosure like this isn't just a curiosity — it's a useful starting point for attackers. Enumerating usernames might seem harmless, but it's often the first step in mapping out a target's footprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tie a wallet to a real identity (doxxing vector)&lt;/li&gt;
&lt;li&gt;Correlate multiple wallets to one person&lt;/li&gt;
&lt;li&gt;Build a profile for targeted phishing or social engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For NFT holders, that's a real privacy concern. Your wallet is supposed to be pseudonymous — this tool weakens that pseudonymity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenSea Did Right
&lt;/h2&gt;

&lt;p&gt;To be fair, not everything on that server was exposed. The sensitive tools — like &lt;code&gt;cancel_orders&lt;/code&gt; — properly required a wallet JWT with &lt;code&gt;write:orders&lt;/code&gt; scope. Trying to call them without it returned a clean &lt;code&gt;MISSING_SCOPE&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;So the architecture is sound. The gap was in the &lt;em&gt;discovery&lt;/em&gt; layer: a handful of tools were reachable with just an API key that anyone could mint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Responsible Disclosure
&lt;/h2&gt;

&lt;p&gt;I reported this to OpenSea's Bugcrowd program with a full write-up and PoC. It's now in their triage queue (self-assessed as P4 informational — username enumeration; official severity pending triage).&lt;/p&gt;

&lt;p&gt;I have no affiliation with OpenSea or Bugcrowd.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a security researcher:&lt;/strong&gt; don't go poking at this yourself. It's been reported, and testing live systems without authorization is both against program rules and a great way to get banned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free Recon Kit
&lt;/h2&gt;

&lt;p&gt;I turned the whole investigation into a free PDF — &lt;strong&gt;the OpenSea MCP Server Recon Kit&lt;/strong&gt; — with the methodology, the tools I enumerated, and the lessons learned. It's pay-what-you-want (free, $0 minimum), and it's the same playbook I use when auditing any undocumented API surface. The kit contains methodology only — no exploit code:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/opensea-mcp-recon-kit" rel="noopener noreferrer"&gt;Download the free recon kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;Undocumented internal APIs are everywhere. They're often overlooked during security reviews, yet they can leak exactly the kind of secrets — like API keys — that turn a minor misconfiguration into a real problem.&lt;/p&gt;

&lt;p&gt;The next time you're auditing a target, ask yourself: &lt;em&gt;what endpoints exist that nobody documented?&lt;/em&gt; That's where the interesting stuff hides.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This technique was reported to Bugcrowd and is shared here for educational purposes only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this useful? Follow me for more bug bounty write-ups and recon methodology.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>bugbounty</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Found an Undocumented MCP Server on OpenSea — and It Leaked Usernames for Any Wallet</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Sat, 12 Sep 2026 23:52:09 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-5935</link>
      <guid>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-5935</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; OpenSea runs an undocumented MCP server at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;. One of its tools hands out API keys to anyone who asks, and another lets you resolve any Ethereum address &lt;em&gt;with an OpenSea profile&lt;/em&gt; to its owner's username. Reported to Bugcrowd, now sharing the story and a free recon kit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was poking around OpenSea's tooling one afternoon — just trying to understand how some of their internal features worked. Nothing fancy, no scanners, no brute force. Just curiosity and a browser.&lt;/p&gt;

&lt;p&gt;That's when I stumbled onto something that wasn't meant for public eyes: an undocumented MCP server sitting at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first glance it looked like just another internal API. But one particular tool caught my attention: &lt;code&gt;get_instant_api_key&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;Turns out, that little endpoint was an unintended exposure of something far more valuable. By sending a straightforward POST request to &lt;code&gt;https://api.opensea.io/api/v2/auth/keys&lt;/code&gt;, I could pull a valid API key straight out of the server. No authentication hoops — just a direct line to a credential that should have stayed hidden.&lt;/p&gt;

&lt;p&gt;With that key in hand, I called the &lt;code&gt;get_profile&lt;/code&gt; tool and, suddenly, I could look up a username for any Ethereum address with an OpenSea profile. I tested it on my own wallet, then on a well-known one. Both resolved instantly.&lt;/p&gt;

&lt;p&gt;It felt like finding a master key to a building you weren't supposed to enter.&lt;/p&gt;

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

&lt;p&gt;Information disclosure like this isn't just a curiosity — it's a goldmine for attackers. Enumerating usernames might seem harmless, but it's often the first step in mapping out a target's footprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tie a wallet to a real identity (doxxing vector)&lt;/li&gt;
&lt;li&gt;Correlate multiple wallets to one person&lt;/li&gt;
&lt;li&gt;Build a profile for targeted phishing or social engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For NFT holders, that's a real privacy concern. Your wallet is supposed to be pseudonymous — this tool quietly erases that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenSea Did Right
&lt;/h2&gt;

&lt;p&gt;To be fair, not everything on that server was exposed. The sensitive tools — like &lt;code&gt;cancel_orders&lt;/code&gt; — properly required a wallet JWT with &lt;code&gt;write:orders&lt;/code&gt; scope. Trying to call them without it returned a clean &lt;code&gt;MISSING_SCOPE&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;So the architecture is sound. The gap was in the &lt;em&gt;discovery&lt;/em&gt; layer: a handful of tools were reachable with just an API key that anyone could mint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Responsible Disclosure
&lt;/h2&gt;

&lt;p&gt;I reported this to OpenSea's Bugcrowd program with a full write-up and PoC. It's now in their triage queue (self-assessed as P4 informational — username enumeration; official severity pending triage).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a security researcher:&lt;/strong&gt; don't go poking at this yourself. It's been reported, and testing live systems without authorization is both against program rules and a great way to get banned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free Recon Kit
&lt;/h2&gt;

&lt;p&gt;I turned the whole investigation into a free PDF — &lt;strong&gt;the OpenSea MCP Server Recon Kit&lt;/strong&gt; — with the methodology, the tools I enumerated, and the lessons learned. It's pay-what-you-want (free, $0 minimum), and it's the same playbook I use when auditing any undocumented API surface. The kit contains methodology only — no exploit code:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/opensea-mcp-recon-kit" rel="noopener noreferrer"&gt;Download the free recon kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;Undocumented internal APIs are everywhere. They're often overlooked during security reviews, yet they can leak exactly the kind of secrets — like API keys — that turn a minor misconfiguration into a real problem.&lt;/p&gt;

&lt;p&gt;The next time you're auditing a target, ask yourself: &lt;em&gt;what endpoints exist that nobody documented?&lt;/em&gt; That's where the interesting stuff hides.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This technique was reported to Bugcrowd and is shared here for educational purposes only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this useful? Follow me for more bug bounty write-ups and recon methodology.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>I Rewrote 5 AI-Generated Paragraphs to Sound Human — Here's the Before &amp; After</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Thu, 10 Sep 2026 00:13:37 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-rewrote-5-ai-generated-paragraphs-to-sound-human-heres-the-before-after-3i0p</link>
      <guid>https://dev.to/tinycoder-studio/i-rewrote-5-ai-generated-paragraphs-to-sound-human-heres-the-before-after-3i0p</guid>
      <description>&lt;p&gt;Last week I wrote about building an AI humanizer that actually sounds human. &lt;a class="mentioned-user" href="https://dev.to/topstar_ai"&gt;@topstar_ai&lt;/a&gt; pointed out the hardest challenge — and they're right. The hardest part isn't making AI text longer. It's making it feel like someone actually meant what they wrote.&lt;/p&gt;

&lt;p&gt;Here's what I found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Uniform rhythm.&lt;/strong&gt; Every sentence is 12-18 words. Humans vary wildly — some thoughts are three words, others spill across two sentences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Hedging and filler.&lt;/strong&gt; "It's important to note that..." "In today's world..." These phrases add zero meaning and scream template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Empty transitions.&lt;/strong&gt; "Furthermore," "Moreover," "Additionally" — humans say "also" or just skip the transition entirely.&lt;/p&gt;

&lt;p&gt;None of these are wrong. They're just &lt;em&gt;empty&lt;/em&gt;. And once you spot them, you can't unsee them.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Product description (generic SaaS pitch)&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; Our platform provides a comprehensive suite of tools designed to help teams collaborate more effectively and streamline their workflow.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; It helps teams stop chasing each other across Slack threads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Cut the feature list. Gave it a problem and a punch.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;2. Blog intro (the classic "In this article, we'll explore...")&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; In this article, we will explore the top trends shaping the future of web development in 2025 and beyond.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Web dev moved fast in 2024. Here's what actually mattered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Killed the meta-commentary. Started mid-thought like a human would.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. Social media post (LinkedIn thought-leadership)&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; Furthermore, it is important to recognize that leadership in today's world requires adaptability and a commitment to continuous learning.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Adapt or die. That's been true forever — but most people still skip the uncomfortable part.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Removed "furthermore" and "it is important to recognize." Added an opinion.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. Email outreach (cold template)&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; I hope this message finds you well. I am reaching out to introduce our product, which I believe could be of great value to your organization.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Your site's load time is 4 seconds. Here's how to cut it to 1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; No greeting, no filler. Led with a specific number they can't ignore.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;5. Error message / technical explanation&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; An unexpected error has occurred. Please try again or contact support if the issue persists.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Something broke on our end. We're fixing it — refresh in 30 seconds and it should work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Added a timeframe and ownership. Humans explain what's happening and when it ends.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Human writing has opinions, rhythm changes, and friction. AI writing is smooth, safe, and forgettable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole game. If you're tired of generating text that reads like a committee wrote it, check out &lt;strong&gt;PseudoHuman&lt;/strong&gt; at &lt;a href="https://toolbox-lilac-three.vercel.app" rel="noopener noreferrer"&gt;toolbox-lilac-three.vercel.app&lt;/a&gt; — it takes your AI draft and injects the personality it's missing.&lt;/p&gt;

&lt;p&gt;What's the tell-tale sign that makes &lt;em&gt;you&lt;/em&gt; stop reading something that sounds AI-generated?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I'm Building an AI Humanizer That Actually Sounds Human — Here's What I'm Learning</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:22:02 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/im-building-an-ai-humanizer-that-actually-sounds-human-heres-what-im-learning-38kd</link>
      <guid>https://dev.to/tinycoder-studio/im-building-an-ai-humanizer-that-actually-sounds-human-heres-what-im-learning-38kd</guid>
      <description>&lt;p&gt;Hey — I'm building an AI humanizer in public, and I wanted to share what I'm learning before there's anything to launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I'm seeing
&lt;/h2&gt;

&lt;p&gt;Most "humanizers" I've tried just swap synonyms. You know the output instantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Utilize" instead of "use." Perfect grammar. Zero rhythm. Every sentence the same length.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It passes a thesaurus check and fails the human check. Real people write messy — fragments, dashes, short punches after long rambles. That's what I'm trying to capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building (honestly, still early)
&lt;/h2&gt;

&lt;p&gt;No beta yet. No launch. I'm building a small tool that rewrites AI-sounding text so it aims to sound like a person wrote it — rhythm, burstiness, tone shifts, not just word swaps.&lt;/p&gt;

&lt;p&gt;Right now I'm just testing in the open and learning from real examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me so far
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rhythm beats vocabulary.&lt;/strong&gt; Varying sentence length does more for "sounds human" than any synonym swap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imperfection is a feature.&lt;/strong&gt; A fragment. An em dash. A blunt short sentence. Readers trust it more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context matters more than cleverness.&lt;/strong&gt; Same input needs a different rewrite for an essay vs. a tweet vs. an email.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A tiny before/after (from my own testing notes, still rough)
&lt;/h2&gt;

&lt;p&gt;Before (typical AI):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"In today's fast-paced world, it is important to utilize effective tools for content creation."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What I'm aiming for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Everyone's drowning in AI-sounding content. I'm trying to make mine sound like I actually wrote it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Still rough. That's the point of building in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want from you
&lt;/h2&gt;

&lt;p&gt;Drop a sentence that &lt;em&gt;screams&lt;/em&gt; AI in the comments — I'll reply with how I'd approach rewriting it, and what I'd still get wrong. Teaching in the open, learning in the open.&lt;/p&gt;

&lt;p&gt;If you want a note when the early access list opens, follow along. No spam, just the build log.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>buildinpublic</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Vanilla JavaScript PWA Tutorial: Build With Zero Dependencies</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Mon, 07 Sep 2026 10:05:39 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/vanilla-javascript-pwa-tutorial-build-with-zero-dependencies-1cpj</link>
      <guid>https://dev.to/tinycoder-studio/vanilla-javascript-pwa-tutorial-build-with-zero-dependencies-1cpj</guid>
      <description>&lt;p&gt;Every PWA tutorial starts with "install React, then Next.js, then configure Webpack..." By the time you're done, you have 200MB of node_modules for a to-do app.&lt;/p&gt;

&lt;p&gt;This tutorial uses zero dependencies. Just HTML, CSS, and JavaScript. You'll have a working PWA in 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;A simple web app that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;li&gt;Installs to the home screen&lt;/li&gt;
&lt;li&gt;Has a service worker for caching&lt;/li&gt;
&lt;li&gt;Runs on any static host (Vercel, Netlify, GitHub Pages)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: The HTML
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My PWA&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"manifest"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/manifest.json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"theme-color"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"#0F172A"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"apple-touch-icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/icon-192.png"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello, PWA!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This app works offline.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: The Manifest
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My PWA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PWA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#0F172A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"theme_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#06B6D4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&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="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&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="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icon-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&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;h2&gt;
  
  
  Step 3: The Service Worker
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;sw.js&lt;/code&gt;:&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;CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-pwa-v1&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;ASSETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/app.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/style.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE&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;cache&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;keys&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;keys&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;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;CACHE&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;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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;The service worker does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; — caches all app files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activate&lt;/strong&gt; — deletes old caches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch&lt;/strong&gt; — serves from cache, falls back to network&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Register the Service Worker
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app.js&lt;/code&gt;:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reg&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SW registered:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SW failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;
  
  
  Step 5: Deploy
&lt;/h2&gt;

&lt;p&gt;If you're using Vercel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; vercel
vercel deploy &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your PWA is live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Offline
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open Chrome DevTools → Application → Service Workers&lt;/li&gt;
&lt;li&gt;Check "Offline" in the Network tab&lt;/li&gt;
&lt;li&gt;Refresh the page — it should still load&lt;/li&gt;
&lt;li&gt;On Android: three-dot menu → "Add to Home Screen"&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Vanilla JS?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — nothing to update, nothing to break&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — no build step, no transpilation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small&lt;/strong&gt; — the entire app is under 10KB gzipped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understandable&lt;/strong&gt; — you can read every line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React and Next.js are great for complex apps. But for a simple PWA? Vanilla JS is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want a head start?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/gyhehh?utm_source=devto&amp;amp;utm_medium=vanilla-pwa&amp;amp;utm_campaign=lead_gen_2026" rel="noopener noreferrer"&gt;TinyCoder&lt;/a&gt; is a vanilla JS PWA with 5 offline dev tools. Config-driven, deployable in minutes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://toolbox-lilac-three.vercel.app/blog/vanilla-js-pwa-tutorial.html" rel="noopener noreferrer"&gt;toolbox-lilac-three.vercel.app/blog/vanilla-js-pwa-tutorial.html&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
