<?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: Ben Greenberg</title>
    <description>The latest articles on DEV Community by Ben Greenberg (@bengreenberg).</description>
    <link>https://dev.to/bengreenberg</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%2F29526%2Fab3873ff-b15d-48ee-90c2-0006c40df4a1.jpg</url>
      <title>DEV Community: Ben Greenberg</title>
      <link>https://dev.to/bengreenberg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bengreenberg"/>
    <language>en</language>
    <item>
      <title>Turn a local skill library into agent-ready documentation</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Wed, 24 Jun 2026 11:32:16 +0000</pubDate>
      <link>https://dev.to/bengreenberg/turn-a-local-skill-library-into-agent-ready-documentation-10c8</link>
      <guid>https://dev.to/bengreenberg/turn-a-local-skill-library-into-agent-ready-documentation-10c8</guid>
      <description>&lt;p&gt;Most agent setups start with a pile of useful local skills.&lt;/p&gt;

&lt;p&gt;That pile usually makes sense to the person who built it. There is a skill for GitHub, one for writing, one for reminders, one for privacy filtering, one for making diagrams, one for checking session logs, one for working with canvases, one for creating new skills, and so on.&lt;/p&gt;

&lt;p&gt;The problem shows up later, when a future agent has to decide what to do.&lt;/p&gt;

&lt;p&gt;A local skill library answers the question, “What can this environment do?”&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;AGENTS.md&lt;/code&gt; file should answer a different question:&lt;/p&gt;

&lt;p&gt;When should an agent use each capability, what should it read first, what tools are safe to call, and what boundaries should it respect?&lt;/p&gt;

&lt;p&gt;That difference matters. A directory full of skills is an inventory. Agent-ready documentation is an operating map.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fppyts074mnxtta05392q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fppyts074mnxtta05392q.png" alt="A flow showing how a raw local skill library becomes an agent-ready AGENTS.md operating map" width="600" height="26"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tutorial uses an OpenClaw-style skill library as the worked example, but the same pattern applies to any local agent setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with capabilities, not files
&lt;/h2&gt;

&lt;p&gt;A common mistake is to document skills in filesystem order.&lt;/p&gt;

&lt;p&gt;That gives you something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; github
&lt;span class="p"&gt;-&lt;/span&gt; writing-style-skill
&lt;span class="p"&gt;-&lt;/span&gt; privacy-filter
&lt;span class="p"&gt;-&lt;/span&gt; remind-me
&lt;span class="p"&gt;-&lt;/span&gt; canvas
&lt;span class="p"&gt;-&lt;/span&gt; skill-creator
&lt;span class="p"&gt;-&lt;/span&gt; session-logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That list is technically true, but it does not help an agent make decisions.&lt;/p&gt;

&lt;p&gt;A better first pass groups skills by the work they enable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Capability map&lt;/span&gt;

&lt;span class="gu"&gt;### Development workflows&lt;/span&gt;

Use these when the user asks about code, repositories, issues, pull requests, debugging, or project maintenance.
&lt;span class="p"&gt;
-&lt;/span&gt; github: interact with GitHub through the &lt;span class="sb"&gt;`gh`&lt;/span&gt; CLI
&lt;span class="p"&gt;-&lt;/span&gt; gh-issues: fetch issues, select candidates, delegate fixes, and open PRs
&lt;span class="p"&gt;-&lt;/span&gt; node-inspect-debugger: debug Node.js processes
&lt;span class="p"&gt;-&lt;/span&gt; python-debugpy: debug Python code

&lt;span class="gu"&gt;### Writing and publishing&lt;/span&gt;

Use these when the user asks for drafts, essays, social posts, newsletters, or style-sensitive writing.
&lt;span class="p"&gt;
-&lt;/span&gt; writing-style-skill: draft in the user's preferred voice
&lt;span class="p"&gt;-&lt;/span&gt; blog-drafter: create blog drafts
&lt;span class="p"&gt;-&lt;/span&gt; x-posts: optimize short-form posts and threads

&lt;span class="gu"&gt;### Safety and privacy&lt;/span&gt;

Use these when content may contain personal data, secrets, or sensitive context.
&lt;span class="p"&gt;
-&lt;/span&gt; privacy-filter: redact PII from text or files
&lt;span class="p"&gt;-&lt;/span&gt; 1password: retrieve secrets through the approved CLI flow

&lt;span class="gu"&gt;### Personal automation&lt;/span&gt;

Use these when the user asks for reminders, home devices, calendar checks, or recurring background tasks.
&lt;span class="p"&gt;
-&lt;/span&gt; remind-me: create one-time reminders
&lt;span class="p"&gt;-&lt;/span&gt; taskflow: coordinate longer detached jobs
&lt;span class="p"&gt;-&lt;/span&gt; weather: check current weather and forecasts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns the library into a decision surface. The agent no longer has to infer that &lt;code&gt;writing-style-skill&lt;/code&gt; is relevant to an essay draft, or that &lt;code&gt;privacy-filter&lt;/code&gt; should be considered before sharing text externally. The map says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add triggers
&lt;/h2&gt;

&lt;p&gt;A capability map becomes much more useful when each section includes triggers.&lt;/p&gt;

&lt;p&gt;Triggers are short descriptions of the user intent that should cause the agent to load a skill.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Writing and publishing&lt;/span&gt;

Load &lt;span class="sb"&gt;`writing-style-skill`&lt;/span&gt; when the user asks to:
&lt;span class="p"&gt;
-&lt;/span&gt; draft an article, essay, email, talk abstract, or newsletter
&lt;span class="p"&gt;-&lt;/span&gt; rewrite something in their voice
&lt;span class="p"&gt;-&lt;/span&gt; adapt technical content for developers
&lt;span class="p"&gt;-&lt;/span&gt; make a draft sound less generic

Load &lt;span class="sb"&gt;`x-posts`&lt;/span&gt; when the user asks to:
&lt;span class="p"&gt;
-&lt;/span&gt; write a post for X
&lt;span class="p"&gt;-&lt;/span&gt; turn an idea into a thread
&lt;span class="p"&gt;-&lt;/span&gt; improve a short social post for reach or clarity

Load &lt;span class="sb"&gt;`blog-drafter`&lt;/span&gt; only when the user explicitly wants a blog draft created.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line matters. Some skills only produce local context. Others take external action. &lt;code&gt;blog-drafter&lt;/code&gt; crosses that boundary because it creates a draft in a publishing system. The agent needs to know that this should not happen casually.&lt;/p&gt;

&lt;p&gt;Good triggers are concrete. Bad triggers are vague.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Use this for content.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Use this when drafting or revising long-form writing, especially when tone and structure matter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Load &lt;span class="sb"&gt;`writing-style-skill`&lt;/span&gt; before drafting blog posts, essays, conference abstracts, newsletter sections, or public-facing technical explanations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The best version tells the agent what the user might actually say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Document the first file to read
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbf50wopkpcsyvil0tkbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbf50wopkpcsyvil0tkbi.png" alt="A sequence showing how an agent routes a user request through AGENTS.md, loads the relevant skill, and then acts or asks for confirmation" width="600" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many local skills have their own &lt;code&gt;SKILL.md&lt;/code&gt;. An agent should not guess from the skill name alone. The root &lt;code&gt;AGENTS.md&lt;/code&gt; should tell the agent which file is authoritative.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Skill loading rule&lt;/span&gt;

When a task matches a skill, read that skill's &lt;span class="sb"&gt;`SKILL.md`&lt;/span&gt; before taking action.

Do not rely only on the skill name or description. The skill file may include safety rules, required tools, local paths, examples, or external-action limits.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a local OpenClaw skill set, this is especially useful because different skills have different operational shapes.&lt;/p&gt;

&lt;p&gt;A writing skill might mostly contain tone rules and examples.&lt;/p&gt;

&lt;p&gt;A GitHub skill might specify preferred &lt;code&gt;gh&lt;/code&gt; commands and review behavior.&lt;/p&gt;

&lt;p&gt;A home automation skill might include device-specific constraints.&lt;/p&gt;

&lt;p&gt;A privacy skill might define which data must be filtered before sharing.&lt;/p&gt;

&lt;p&gt;The root file does not need to duplicate all of that. It needs to make skill loading mandatory and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate internal work from external action
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5s9u8k9s00e70t257r54.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5s9u8k9s00e70t257r54.png" alt="A decision flow separating internal work from external actions that require clear user intent or confirmation" width="224" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Agent documentation should make one distinction very clear:&lt;/p&gt;

&lt;p&gt;Reading, drafting, searching, and organizing are internal actions.&lt;/p&gt;

&lt;p&gt;Sending, posting, publishing, deleting, buying, messaging, or changing real-world systems are external actions.&lt;/p&gt;

&lt;p&gt;That boundary belongs near the top of &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## External action boundary&lt;/span&gt;

The agent may freely:
&lt;span class="p"&gt;
-&lt;/span&gt; read local files
&lt;span class="p"&gt;-&lt;/span&gt; inspect skill documentation
&lt;span class="p"&gt;-&lt;/span&gt; draft text
&lt;span class="p"&gt;-&lt;/span&gt; run non-destructive local checks
&lt;span class="p"&gt;-&lt;/span&gt; prepare changes for review

The agent must ask before:
&lt;span class="p"&gt;
-&lt;/span&gt; sending email or messages
&lt;span class="p"&gt;-&lt;/span&gt; publishing posts
&lt;span class="p"&gt;-&lt;/span&gt; creating public drafts
&lt;span class="p"&gt;-&lt;/span&gt; deleting data
&lt;span class="p"&gt;-&lt;/span&gt; changing account settings
&lt;span class="p"&gt;-&lt;/span&gt; controlling physical devices
&lt;span class="p"&gt;-&lt;/span&gt; spending money
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For OpenClaw-style environments, this boundary keeps powerful skills usable without making them reckless. A &lt;code&gt;github&lt;/code&gt; skill that reads issues is different from one that opens a pull request. A &lt;code&gt;blog&lt;/code&gt; skill that drafts locally is different from one that creates a draft in an external service. A &lt;code&gt;govee&lt;/code&gt; or &lt;code&gt;homeconnect&lt;/code&gt; skill can affect the physical environment.&lt;/p&gt;

&lt;p&gt;Write those distinctions down.&lt;/p&gt;

&lt;p&gt;Agents are much better when they do not have to reconstruct your risk model from vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture tool boundaries inside each capability
&lt;/h2&gt;

&lt;p&gt;A capability map should not only say what a skill does. It should say what the agent should avoid doing.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Privacy and redaction&lt;/span&gt;

Use &lt;span class="sb"&gt;`privacy-filter`&lt;/span&gt; before sharing user-provided text outside the local workspace when it may contain:
&lt;span class="p"&gt;
-&lt;/span&gt; names
&lt;span class="p"&gt;-&lt;/span&gt; addresses
&lt;span class="p"&gt;-&lt;/span&gt; phone numbers
&lt;span class="p"&gt;-&lt;/span&gt; email addresses
&lt;span class="p"&gt;-&lt;/span&gt; account identifiers
&lt;span class="p"&gt;-&lt;/span&gt; private messages
&lt;span class="p"&gt;-&lt;/span&gt; internal business context

Do not paste raw private content into public channels or external tools unless the user explicitly approves it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## GitHub work&lt;/span&gt;

Use &lt;span class="sb"&gt;`github`&lt;/span&gt; for repository, issue, pull request, and CI work.

Safe without asking:
&lt;span class="p"&gt;
-&lt;/span&gt; inspect issues and PRs
&lt;span class="p"&gt;-&lt;/span&gt; read CI logs
&lt;span class="p"&gt;-&lt;/span&gt; check branch status
&lt;span class="p"&gt;-&lt;/span&gt; prepare local patches

Ask before:
&lt;span class="p"&gt;
-&lt;/span&gt; opening a PR
&lt;span class="p"&gt;-&lt;/span&gt; commenting publicly
&lt;span class="p"&gt;-&lt;/span&gt; closing issues
&lt;span class="p"&gt;-&lt;/span&gt; pushing branches to shared remotes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part many local docs miss. They document how to use a tool, but not when to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Include sequencing rules
&lt;/h2&gt;

&lt;p&gt;Some skills should run before others.&lt;/p&gt;

&lt;p&gt;In the OpenClaw set, &lt;code&gt;writing-style-skill&lt;/code&gt; should load before drafting. &lt;code&gt;privacy-filter&lt;/code&gt; should run before sharing sensitive text externally. &lt;code&gt;skill-creator&lt;/code&gt; should load before changing skill files. &lt;code&gt;github&lt;/code&gt; should load before acting on GitHub state.&lt;/p&gt;

&lt;p&gt;That can be expressed as simple sequencing rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Sequencing rules&lt;/span&gt;

Before drafting public writing, load &lt;span class="sb"&gt;`writing-style-skill`&lt;/span&gt;.

Before creating or modifying a reusable skill, load &lt;span class="sb"&gt;`skill-creator`&lt;/span&gt;.

Before using private text in an external channel, consider &lt;span class="sb"&gt;`privacy-filter`&lt;/span&gt;.

Before taking GitHub action, load &lt;span class="sb"&gt;`github`&lt;/span&gt; and inspect the current repository state.

Before using a tool that affects the outside world, confirm the user intended that action.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These rules save agents from doing work in the wrong order.&lt;/p&gt;

&lt;p&gt;A good &lt;code&gt;AGENTS.md&lt;/code&gt; does not need to cover every possible path. It should cover the paths where ordering matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give agents examples of correct routing
&lt;/h2&gt;

&lt;p&gt;Examples are often more useful than rules.&lt;/p&gt;

&lt;p&gt;Here is a compact routing table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Routing examples&lt;/span&gt;

User asks: "Draft this as a technical blog post."
Use: &lt;span class="sb"&gt;`writing-style-skill`&lt;/span&gt;
Do: draft in the user's preferred voice
Do not: publish it anywhere

User asks: "Turn this into a blog draft."
Use: &lt;span class="sb"&gt;`writing-style-skill`&lt;/span&gt;, then &lt;span class="sb"&gt;`blog-drafter`&lt;/span&gt;
Do: prepare the content first
Ask before: creating the external draft, unless the user clearly requested that exact action

User asks: "Can you fix this GitHub issue?"
Use: &lt;span class="sb"&gt;`github`&lt;/span&gt;, possibly &lt;span class="sb"&gt;`gh-issues`&lt;/span&gt;
Do: inspect the issue, read the repo, make local changes, run tests
Ask before: opening a PR if the instruction was ambiguous

User asks: "Remember this workflow as a reusable skill."
Use: &lt;span class="sb"&gt;`skill-creator`&lt;/span&gt;
Do: create or update the skill through the approved skill workflow
Do not: hand-edit skill proposal state if the environment has a dedicated tool for that

User asks: "Share this private message in a public post."
Use: &lt;span class="sb"&gt;`privacy-filter`&lt;/span&gt;
Do: redact or summarize safely
Ask before: publishing or sending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not to create a huge rules engine. The point is to give future agents enough examples to route the next request correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep local notes out of shared skills
&lt;/h2&gt;

&lt;p&gt;A reusable skill should explain general behavior. Local environment details belong somewhere else.&lt;/p&gt;

&lt;p&gt;For OpenClaw-style workspaces, that might mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;SKILL.md       -&amp;gt; reusable instructions
TOOLS.md       -&amp;gt; local device names, account aliases, hostnames, personal setup notes
AGENTS.md     -&amp;gt; workspace-level operating rules
MEMORY.md     -&amp;gt; durable user and project context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation keeps skills portable.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;weather&lt;/code&gt; skill can explain how weather lookup works. The local notes can say which city is usually relevant. A &lt;code&gt;canvas&lt;/code&gt; skill can explain how to present HTML on connected nodes. Local notes can say which node names exist in this setup.&lt;/p&gt;

&lt;p&gt;That distinction is small, but it keeps a skill library from turning into a private config dump.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make safety boundaries visible, not buried
&lt;/h2&gt;

&lt;p&gt;If a skill can send, post, delete, control, spend, or expose private data, say so in the capability map.&lt;/p&gt;

&lt;p&gt;A practical format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## High-risk skills&lt;/span&gt;

These skills can affect external systems or expose private data. Load their &lt;span class="sb"&gt;`SKILL.md`&lt;/span&gt; and confirm intent before using them for external action.
&lt;span class="p"&gt;
-&lt;/span&gt; blog-drafter: creates drafts in a publishing account
&lt;span class="p"&gt;-&lt;/span&gt; imsg: can send iMessage/SMS
&lt;span class="p"&gt;-&lt;/span&gt; work-slack: reads workplace Slack data
&lt;span class="p"&gt;-&lt;/span&gt; gog/work-google: access personal or work Google data
&lt;span class="p"&gt;-&lt;/span&gt; govee/homeconnect/sensibo/switchbot: control physical devices
&lt;span class="p"&gt;-&lt;/span&gt; 1password: accesses secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not mean the skills are unsafe. It means they are powerful.&lt;/p&gt;

&lt;p&gt;Agents do better with clear labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the root AGENTS.md as an operating manual
&lt;/h2&gt;

&lt;p&gt;A useful &lt;code&gt;AGENTS.md&lt;/code&gt; should be short enough to read and strong enough to steer behavior.&lt;/p&gt;

&lt;p&gt;A good structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# AGENTS.md&lt;/span&gt;

&lt;span class="gu"&gt;## Role&lt;/span&gt;

You are working inside this workspace. Read local instructions before acting. Prefer existing skills and tools over inventing new workflows.

&lt;span class="gu"&gt;## Startup&lt;/span&gt;

Read:
&lt;span class="p"&gt;
-&lt;/span&gt; SOUL.md
&lt;span class="p"&gt;-&lt;/span&gt; USER.md
&lt;span class="p"&gt;-&lt;/span&gt; recent daily memory files
&lt;span class="p"&gt;-&lt;/span&gt; MEMORY.md when in a direct private session

&lt;span class="gu"&gt;## Capability map&lt;/span&gt;

Group available skills by task:
&lt;span class="p"&gt;
-&lt;/span&gt; development workflows
&lt;span class="p"&gt;-&lt;/span&gt; writing and publishing
&lt;span class="p"&gt;-&lt;/span&gt; privacy and safety
&lt;span class="p"&gt;-&lt;/span&gt; personal automation
&lt;span class="p"&gt;-&lt;/span&gt; debugging and inspection
&lt;span class="p"&gt;-&lt;/span&gt; media and presentation
&lt;span class="p"&gt;-&lt;/span&gt; skill maintenance

&lt;span class="gu"&gt;## Skill loading&lt;/span&gt;

When a task matches a skill, read that skill's &lt;span class="sb"&gt;`SKILL.md`&lt;/span&gt; before acting.

&lt;span class="gu"&gt;## External action boundary&lt;/span&gt;

Internal work is allowed. External action requires clear user intent or confirmation.

&lt;span class="gu"&gt;## Safety rules&lt;/span&gt;

Protect private data. Prefer recoverable actions. Do not run destructive commands casually.

&lt;span class="gu"&gt;## Routing examples&lt;/span&gt;

Include examples of common user requests and the skills they should trigger.

&lt;span class="gu"&gt;## Local notes&lt;/span&gt;

Put machine-specific details in &lt;span class="sb"&gt;`TOOLS.md`&lt;/span&gt;, not in reusable skills.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough to turn a pile of local skills into a working agent interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat AGENTS.md as a map, not a museum
&lt;/h2&gt;

&lt;p&gt;The file should change as the skill library changes.&lt;/p&gt;

&lt;p&gt;When you add a new skill, add it to the capability map. When a tool gains external side effects, move it into the high-risk section. When an agent makes a routing mistake, add a small example that would have prevented it.&lt;/p&gt;

&lt;p&gt;The best &lt;code&gt;AGENTS.md&lt;/code&gt; files are not long. They are current.&lt;/p&gt;

&lt;p&gt;For developers building local agent systems, this is the real payoff: your skill library stops being something only you understand. It becomes a documented capability layer that future agents can read, reason over, and use correctly.&lt;/p&gt;

&lt;p&gt;That is what agent-ready documentation is for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>learning</category>
      <category>agents</category>
    </item>
    <item>
      <title>An AI agent that pays for its own API calls on AWS</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Fri, 29 May 2026 11:27:13 +0000</pubDate>
      <link>https://dev.to/bengreenberg/an-ai-agent-that-pays-for-its-own-api-calls-on-aws-din</link>
      <guid>https://dev.to/bengreenberg/an-ai-agent-that-pays-for-its-own-api-calls-on-aws-din</guid>
      <description>&lt;p&gt;I built a small AWS Bedrock AgentCore agent that pays for a paywalled API with real USDC on Arbitrum One. It asks for a report, gets back HTTP 402 Payment Required, settles the charge on its own, and retries. No API key, no card on file, no human clicking approve. The settlement lands on Arbitrum One mainnet, and the run prints an Arbiscan link to the transaction so you can read it yourself.&lt;/p&gt;

&lt;p&gt;There's a great walkthrough of the same idea on Base Sepolia by &lt;a href="https://william.mendozagopar.com/blog/bedrock-agentcore-x402.html" rel="noopener noreferrer"&gt;William Mendoza Gopar&lt;/a&gt;. This post expands upon his work: mainnet instead of testnet, real USDC instead of a burn-address demo, and a merchant built on CloudFront, Lambda@Edge, and API Gateway. The full source is at &lt;a href="https://github.com/hummusonrails/arbitrum-x402-aws" rel="noopener noreferrer"&gt;github.com/hummusonrails/arbitrum-x402-aws&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AgentCore gives you
&lt;/h2&gt;

&lt;p&gt;AgentCore is AWS's hosted runtime for AI agents, still in preview. The piece this project leans on is AgentCore Payments: a managed signer that holds an embedded wallet for you and produces payment authorizations on request. Your code asks the PaymentManager to handle a charge, and the private key stays inside AgentCore.&lt;/p&gt;

&lt;p&gt;You stand up four resources once: a PaymentManager with a connector to Coinbase CDP, an embedded crypto wallet as a PaymentInstrument, and a PaymentSession that carries a spend budget and an expiry. After that, paying is a single call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What x402 is
&lt;/h2&gt;

&lt;p&gt;x402 puts the 402 Payment Required status code to work. A paid endpoint answers an unpaid request with 402 and a JSON body describing what it wants: the network, the token, the recipient, and the amount. The client signs an EIP-3009 &lt;code&gt;transferWithAuthorization&lt;/code&gt; for USDC, which lets someone else submit the transfer on-chain, and resends the request with the signature attached. The server verifies and settles through a facilitator, then returns the content. No accounts to create, no keys to rotate, and charges as small as a fraction of a cent. That last property is what lets an agent use it with no human in the loop.&lt;/p&gt;

&lt;p&gt;One aside on the AWS side: this x402 support is a brand-new preview release. This week I worked with the AWS team to make sure it settles across EVM chains, Arbitrum One included, so the wallet, the facilitator, and the settlement path behave the same way on Arbitrum as on any other EVM network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Arbitrum One
&lt;/h2&gt;

&lt;p&gt;An agent that pays per API call cannot spend a dollar in transaction fees to move a fraction of a cent. The math only works if the settlement layer makes sub-cent payments cheap enough to vanish into the cost of the call itself. Arbitrum One clears that bar on price.&lt;/p&gt;

&lt;p&gt;Price is half of it. The other half is predictability. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6etruvxr8th4fheu0vts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6etruvxr8th4fheu0vts.png" alt="Free predictability during all pricing conditions is essential for agentic commerce"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An agent commits to a workflow and runs it; it cannot pause to renegotiate when gas spikes partway through. When fees jump for a few minutes, a person shrugs and waits, but an agent paying per call either overpays or stalls. Arbitrum's gas pricing is built to absorb those spikes. The recent Arbitrum One upgrade replaced the old single-target pricing model, and during peak activity it cut gas by around 98% compared to what that model would have charged. Fees stay low, and they stay close to where they were a minute ago. For x402 micropayments, that stability is worth as much as the headline number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the demo
&lt;/h2&gt;

&lt;p&gt;Three pieces talk to each other. The agent runs on AgentCore with its embedded CDP wallet. The merchant is an AWS stack: CloudFront in front, a Lambda@Edge function on &lt;code&gt;viewer-request&lt;/code&gt; that speaks x402, and an API Gateway HTTP API with a Lambda behind it that holds the report. Settlement runs through the Coinbase CDP facilitator, which broadcasts the USDC transfer on Arbitrum One.&lt;/p&gt;

&lt;p&gt;The round trip looks like this. The agent does a &lt;code&gt;GET /report&lt;/code&gt;. Lambda@Edge sees no payment and returns 402 with the terms. The agent asks AgentCore to produce a payment for those terms and retries. This time Lambda@Edge has a payment header, calls the facilitator to verify and settle, and on success passes the request through to API Gateway, which returns the gated JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent side
&lt;/h2&gt;

&lt;p&gt;The agent code is short because the wallet logic lives in AgentCore. It makes a normal GET. If the response is anything other than 402, it returns it. If it is a 402, it hands the whole challenge, status, headers, and body, to &lt;code&gt;generate_payment_header&lt;/code&gt;, which reads the terms, signs the authorization inside the embedded wallet, and returns the header to attach. Then it retries the GET.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_with_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;payment_instrument_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_session_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;

    &lt;span class="n"&gt;payment_required_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;proof_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payment_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_payment_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payment_instrument_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payment_instrument_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payment_session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payment_session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payment_required_request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payment_required_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;network_preferences&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eip155:42161&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# Arbitrum One
&lt;/span&gt;        &lt;span class="n"&gt;client_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;proof_headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I pass &lt;code&gt;network_preferences=["eip155:42161"]&lt;/code&gt; so the payment targets Arbitrum One rather than relying on a default. The agent never builds EIP-712 typed data and never touches the private key. From the caller's point of view it made one GET and got back a 200 with the report and an Arbiscan link.&lt;/p&gt;

&lt;h2&gt;
  
  
  The merchant side
&lt;/h2&gt;

&lt;p&gt;The payment logic sits in the Lambda@Edge function on &lt;code&gt;viewer-request&lt;/code&gt;. With no payment header it returns the 402 and the &lt;code&gt;accepts[]&lt;/code&gt; terms. With a header it decodes the payment, calls the CDP facilitator's &lt;code&gt;/verify&lt;/code&gt;, then &lt;code&gt;/settle&lt;/code&gt;, and returns the original request so CloudFront continues on to API Gateway. A verify or settle failure comes back as a fresh 402 or a 502.&lt;/p&gt;

&lt;p&gt;One detail to note: CDP's facilitator uses short-lived JWTs that are bound to the exact request URL and method and expire in two minutes. You cannot mint one ahead of time and paste it into config. The edge function signs a fresh JWT with &lt;code&gt;node:crypto&lt;/code&gt; for each verify and settle call, which is also why the CDP key material gets inlined into the edge bundle at build time (Lambda@Edge cannot read environment variables at runtime).&lt;/p&gt;

&lt;p&gt;A design choice worth flagging: this version verifies and settles in &lt;code&gt;viewer-request&lt;/code&gt;, before the origin responds. That keeps the demo to one function and one round trip. For production you want to verify in &lt;code&gt;viewer-request&lt;/code&gt; and settle in &lt;code&gt;viewer-response&lt;/code&gt;, so you only take the money after the content is delivered. &lt;/p&gt;

&lt;h2&gt;
  
  
  Funding it and running it
&lt;/h2&gt;

&lt;p&gt;Setup is one command that creates the AgentCore resources and prints a wallet address. You fund that wallet with USDC on Arbitrum One, grant the agent signing permission, and paste the printed IDs into &lt;code&gt;.env&lt;/code&gt;. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;make run-agent
&lt;span class="gp"&gt;GET https://&amp;lt;merchant&amp;gt;&lt;/span&gt;/report
&lt;span class="go"&gt;  via AgentCore PaymentSession payment-session-...
  using Instrument            payment-instrument-...

Status: 200
Body:
{ ...gated report JSON... }

Arbiscan: https://arbiscan.io/tx/0x...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent requests the report, pays, and prints the JSON plus the transaction link. The whole round trip takes a few seconds, most of it the facilitator talking to the chain. The charge in the repo is 0.01 USDC per call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and the preview caveat
&lt;/h2&gt;

&lt;p&gt;Idle, this costs close to nothing. CloudFront, Lambda@Edge, API Gateway, and AgentCore are billed per use, and the demo's traffic fits inside their free tiers. Per call you pay the 0.01 USDC settlement and a rounding error of compute.&lt;/p&gt;

&lt;p&gt;AgentCore Payments is in preview, so treat it that way. Field names and SDK shapes can move between releases, and I hit a few of those while building this. Pin your versions and re-test after you upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The same pattern extends in two directions worth trying. The agent can pay several different x402 endpoints with the same wallet and session, which makes it a buyer for any priced API it can reach. And the spend budget on the PaymentSession is the natural place to wire an alert or a hard stop, so an agent that misbehaves runs out of allowance instead of running up a bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/hummusonrails/arbitrum-x402-aws" rel="noopener noreferrer"&gt;github.com/hummusonrails/arbitrum-x402-aws&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Base Sepolia walkthrough by William: &lt;a href="https://william.mendozagopar.com/blog/bedrock-agentcore-x402.html" rel="noopener noreferrer"&gt;go to his blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;x402 protocol: &lt;a href="https://www.x402.org" rel="noopener noreferrer"&gt;x402.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bedrock AgentCore: &lt;a href="https://aws.amazon.com/bedrock/agentcore" rel="noopener noreferrer"&gt;aws.amazon.com/bedrock/agentcore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;EIP-3009 (Transfer With Authorization): &lt;a href="https://eips.ethereum.org/EIPS/eip-3009" rel="noopener noreferrer"&gt;eips.ethereum.org/EIPS/eip-3009&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>web3</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I let a kosher lobster run my Shabbat automations</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Thu, 23 Apr 2026 06:22:18 +0000</pubDate>
      <link>https://dev.to/bengreenberg/i-let-a-kosher-lobster-run-my-shabbat-automations-5aln</link>
      <guid>https://dev.to/bengreenberg/i-let-a-kosher-lobster-run-my-shabbat-automations-5aln</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I run an Orthodox Jewish household. That means twenty-five hours a week, every week, the family doesn't touch electronics, doesn't cook, and doesn't adjust the thermostat without thinking about it. Multiply that by the Jewish holiday calendar, where a holiday can chain into Shabbat for two days straight, and you have a real scheduling problem. The fridge needs to be in Sabbath mode before candle lighting and back to normal at the conclusion of the day. The bedroom AC needs a pre-cooled run that ends before sunset. The living room AC needs to know whether it's a heat wave week or not. None of these can be touched once the time starts.&lt;/p&gt;

&lt;p&gt;I built a personal automation stack on top of OpenClaw that handles all of it. The lobster is in charge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddp7kgv0dalmsjffvrip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddp7kgv0dalmsjffvrip.png" alt="The lobster always checks first"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a small wrinkle here that I think is a bit funny. Lobsters are not kosher. The mascot of the open source agent platform managing my religiously observant home is, in fact, one of the most explicitly non-kosher animals. The lobster AI has been very gracious about all of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used OpenClaw
&lt;/h2&gt;

&lt;p&gt;OpenClaw skills are just directories with a SKILL.md and whatever scripts you want the agent to call. That structure made it natural to build each appliance as its own skill and then layer scheduler scripts on top that get triggered by cron jobs not the LLM.&lt;/p&gt;

&lt;p&gt;This split matters. The schedulers don't need an LLM in the loop. They need to be deterministic, idempotent, and survive me forgetting they exist. The agent layer is for the times I message OpenClaw on Telegram and ask "is the fridge already in Sabbath mode?" and want a real answer back.&lt;/p&gt;

&lt;p&gt;Here are two of the schedulers, with the city ID swapped out so you can drop in your own.&lt;/p&gt;

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

&lt;p&gt;This one runs every Friday morning. It pulls candle lighting and havdalah times from &lt;a href="https://www.hebcal.com/" rel="noopener noreferrer"&gt;Hebcal&lt;/a&gt;, then schedules &lt;code&gt;at&lt;/code&gt; jobs to flip the fridge into Sabbath mode an hour before candle lighting and back out at it concludes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pythondef&lt;/span&gt; &lt;span class="nf"&gt;get_shabbat_times&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch this week&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s candle lighting and havdalah times.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HEBCAL_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cfg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;geonameid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MY_CITY_GEONAMEID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;candles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;havdalah&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;parasha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;candles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;havdalah&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;havdalah&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;havdalah&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parashat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;parasha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parasha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;havdalah&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parasha&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the actual scheduling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pythonsabbath_on_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;schedule_at_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sabbath_on_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;schedule_at_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;havdalah&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole shape of it. &lt;a href="https://www.hebcal.com/" rel="noopener noreferrer"&gt;Hebcal&lt;/a&gt; is the single source of truth for times, the appliance skill is the single source of truth for how to talk to the fridge, and the scheduler just glues them together with at.&lt;/p&gt;

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

&lt;p&gt;This is the one that took the most thought, because the Jewish holiday calendar is genuinely hostile to naive scheduling. Rosh Hashana is two days. Yom Tov can start as Shabbat ends and run another full day. Passover has a full holiday day with all restrictions on day one, the intermediate days in the middle with no electroni restrictions, and the holiday fully again on the last day. &lt;/p&gt;

&lt;p&gt;You can't just toggle the fridge per holiday day. You need to know when the whole continuous Sabbath-mode period actually ends.&lt;/p&gt;

&lt;p&gt;So the scheduler walks forward through consecutive holiday days and Shabbat as one block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pythondef&lt;/span&gt; &lt;span class="nf"&gt;find_end_of_period&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yomtov_dates&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Walk forward through consecutive Yom Tov days and Shabbat.
    Handles multi-day Yom Tov, Yom Tov flowing into Shabbat,
    and Shabbat sandwiched between Yom Tov days.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start_date&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;next_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;next_day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;yomtov_dates&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;next_day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_day&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most useful piece, though, is the coordination between schedulers. When a holiday starts as Shabbat is ending, the Shabbat scheduler has already queued an &lt;code&gt;OFF&lt;/code&gt; job for the end of Shabbat. If the holiday scheduler runs and just queues its own &lt;code&gt;ON&lt;/code&gt; job on top, you get a brief window where the fridge exits Sabbath mode and re-enters it, which defeats the point. So the Yom Tov scheduler clears the conflicting Shabbat job before doing its own work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pythonif&lt;/span&gt; &lt;span class="n"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Saturday: Yom Tov starts motzei Shabbat
&lt;/span&gt;    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yom Tov follows Shabbat, fridge already in sabbath mode&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cancelling Shabbat OFF job so sabbath mode stays on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;clear_existing_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shabbat_scheduler.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one block is the kind of thing working through the full complexity of the calendar introduces you to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reminder
&lt;/h2&gt;

&lt;p&gt;Half an hour before candle lighting, OpenClaw sends me a checklist over Telegram. Nothing too complicated, just a quick list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pythonmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shabbat Reminder -- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parasha&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Shabbat Shalom&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Candle lighting in 1 hour at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;candles_local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Havdalah at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;havdalah_local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Checklist:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- Lights&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- Fridge (SabbathMode)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- Door&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- Plata / hot water&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reminder doesn't take any actions on the appliances. The schedulers already did that. It's a human-in-the-loop check that the things requiring human judgement (the door key, the lights I forgot to switch) actually got handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestration layer
&lt;/h2&gt;

&lt;p&gt;Everything is wired together with launchd plists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kosher-lobster.openclaw.cron.shabbat-scheduler.plist fires every Friday morning
kosher-lobster.openclaw.cron.yomtov-scheduler.plist fires daily
kosher-lobster.openclaw.cron.shabbat-ac.plist and shabbat-sleep.plist handle the AC zones
kosher-lobster.openclaw.cron.shabbat-reminder.plist fires the Telegram message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AC schedulers are weather-aware. They pull a forecast, checks against a temperature threshold, and only generates &lt;code&gt;ON/OFF&lt;/code&gt; blocks if the forecast warrants it. The data model shows the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tsinterface&lt;/span&gt; &lt;span class="nx"&gt;ShabbatPlan&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;parasha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;havdalah&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;friday_high_f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;saturday_high_f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;threshold_f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;extreme_heat&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;temp_c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;temp_f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShabbatBlock&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;  &lt;span class="c1"&gt;// ON/OFF time blocks&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// active | skipped | cancelled&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mild Friday in spring? No AC plan generated, no electricity wasted. A heat wave week? Blocks get pre-computed and queued. The scheduler decides; I don't wake up at 5 AM to think about it.&lt;/p&gt;

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

&lt;p&gt;The thing I underestimated going in was how much of OpenClaw's value comes from the boundary it draws between agent-driven work and deterministic work. I started by trying to make OpenClaw as the LLM "decide" when to flip the fridge. That was a bad idea. There's no room here for an agent reasoning incorrectly about a calendar edge case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2k62yc28a1y7kjif0vk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2k62yc28a1y7kjif0vk.png" alt="The lobster welcomes Shabbat"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The right pattern was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scheduler scripts are deterministic, and committable to git&lt;/li&gt;
&lt;li&gt;The skill &lt;code&gt;SKILL.md&lt;/code&gt; files teach the agent how to query state when I ask&lt;/li&gt;
&lt;li&gt;The agent never takes scheduled actions on its own; it answers questions and runs explicit commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenClaw's skill structure made that separation easy to maintain because each skill is just a folder. The schedulers live alongside the skills they call, share the same Python environment, and don't require any agent loop to function. If I uninstalled OpenClaw tomorrow the cron jobs would keep firing and the fridge would keep going into Sabbath mode on time. That's the right resilience property for this kind of automation.&lt;/p&gt;

&lt;p&gt;The other thing I learned is that the agent shines exactly where the schedulers can't help. &lt;/p&gt;

&lt;p&gt;"Is the AC set for this evening?" is a Friday-afternoon question I used to answer by a lot of inquiry into the weather, figuring out what we needed in each room and more. Now I message OpenClaw on Telegram and get the AC schedule shared with me in two seconds. &lt;/p&gt;

&lt;p&gt;The schedulers handle the recurring; the agent handles the one-off. Both of those are first-class in OpenClaw and that's why this works.&lt;/p&gt;

&lt;p&gt;The lobster, against all expectations, has turned out to be the most observant member of the household. He never forgets the time. He never opens the fridge without checking. He has read more of the Jewish calendar API than any of us.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>openclawchallenge</category>
    </item>
    <item>
      <title>Quick tip: canonical URLs prevent silent SEO damage</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Fri, 03 Apr 2026 11:00:16 +0000</pubDate>
      <link>https://dev.to/bengreenberg/quick-tip-canonical-urls-prevent-silent-seo-damage-3460</link>
      <guid>https://dev.to/bengreenberg/quick-tip-canonical-urls-prevent-silent-seo-damage-3460</guid>
      <description>&lt;p&gt;Quick tip on SEO:&lt;/p&gt;

&lt;p&gt;If your site is accessible at both &lt;code&gt;https://example.com&lt;/code&gt; and &lt;code&gt;https://www.example.com&lt;/code&gt;, Google sees two different sites and splits your ranking signals between them.&lt;/p&gt;

&lt;p&gt;Fix: add a canonical tag to every page:&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="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/page"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check if yours is set correctly — the free audit tool at &lt;a href="https://audit.hummusonrails.com/free" rel="noopener noreferrer"&gt;https://audit.hummusonrails.com/free&lt;/a&gt; checks canonical tags along with 4 other key issues.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>devtips</category>
    </item>
    <item>
      <title>Your backend code is a black box. It doesn't have to be.</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Wed, 01 Apr 2026 09:07:54 +0000</pubDate>
      <link>https://dev.to/arbitrum/your-backend-code-is-a-black-box-it-doesnt-have-to-be-59bd</link>
      <guid>https://dev.to/arbitrum/your-backend-code-is-a-black-box-it-doesnt-have-to-be-59bd</guid>
      <description>&lt;p&gt;Your API takes inputs and returns outputs. The logic in between? Nobody outside your team can verify it. Regulators read documentation you wrote about it. Partners call your endpoint and trust the response. Users don't even get that.&lt;/p&gt;

&lt;p&gt;This has worked for decades. But there's a growing category of backend logic where "trust us" isn't enough. Scoring algorithms, compliance checks, eligibility rules, validation pipelines. Anywhere someone needs to independently confirm that your code does what you say it does.&lt;/p&gt;

&lt;p&gt;You don't have a code problem. You have a verifiability problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for publicly auditable code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvblmmb49iy7jrpp5e8h5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvblmmb49iy7jrpp5e8h5.png" alt="Black box vs verifiable compute" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most backend engineers don't think about code auditability because most code doesn't need it. Your CRUD endpoints, your auth flows, your data transformations, those are internal concerns. Nobody outside your org needs to verify them.&lt;/p&gt;

&lt;p&gt;But some functions carry weight. A scoring algorithm that determines loan eligibility. A validation pipeline that checks regulatory compliance. A pricing engine that partners depend on. For these functions, the standard answer is documentation: you write a spec, maybe you share pseudocode, and everyone agrees to trust that the running code matches the paper.&lt;/p&gt;

&lt;p&gt;That's not verification. That's trust with extra steps.&lt;/p&gt;

&lt;p&gt;Publicly auditable code means anyone can call the same function with the same inputs and confirm they get the same outputs. The logic is visible. Execution is deterministic. And the code can't change silently after deployment.&lt;/p&gt;

&lt;p&gt;This isn't a new idea. Open source gives you code visibility. But open source doesn't prove that the code running in production is the same code in the repo. You need a runtime where the deployed code is the source of truth, and where every execution is independently reproducible.&lt;/p&gt;

&lt;p&gt;That runtime exists. And you don't need to learn a new language to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust in, verifiable execution out
&lt;/h2&gt;

&lt;p&gt;When most engineers hear "deploy code onchain," they picture learning Solidity, memorizing gas optimization tricks, and rewriting working logic in an unfamiliar language. That's a reasonable reason to stop listening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.arbitrum.io/stylus/gentle-introduction" rel="noopener noreferrer"&gt;Arbitrum Stylus&lt;/a&gt; skips that detour. You write Rust, compile to WASM, and deploy it to a blockchain that's optimized for low-cost computation. Your existing toolchain, your existing crates, your existing mental model. The contract runs alongside Solidity smart contracts with shared state, but you never need to touch Solidity yourself.&lt;/p&gt;

&lt;p&gt;This isn't about converting you to crypto. It's about giving you a deployment target where execution is publicly verifiable by default.&lt;/p&gt;

&lt;p&gt;Three properties matter here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every execution is independently auditable.&lt;/strong&gt; Anyone can call the contract with the same inputs and confirm they get the same outputs. No trust required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The logic is immutable.&lt;/strong&gt; Once deployed, the code can't change silently. If you push an update, that's a new deployment with its own address and its own history. No "we patched it last Tuesday but forgot to tell you."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution is deterministic.&lt;/strong&gt; Same inputs, same outputs, every time, on every node. No environment-specific drift, no floating point surprises across hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the architecture looks like
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frk0oqvh90439evj9hyur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frk0oqvh90439evj9hyur.png" alt="Scoring engine architecture" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built a &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine" rel="noopener noreferrer"&gt;scoring engine&lt;/a&gt; to test this pattern. The architecture splits cleanly between what stays in your infrastructure and what moves onchain:&lt;/p&gt;

&lt;p&gt;Your backend stays exactly where it is. An Axum API server handles business rules, authentication, request routing. Normal backend work. The only difference is that when the API needs a computation verified, it calls an onchain contract instead of a local function.&lt;/p&gt;

&lt;p&gt;The onchain piece is a Stylus contract. It takes parameters, runs the calculation, and returns results. No state storage, just pure computation. Think of it as a function you deploy instead of host.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1w298ib5f7tb8xt2djjc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1w298ib5f7tb8xt2djjc.png" alt="Shared crate compilation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A shared Rust crate holds the type definitions and compiles for both environments. The contract uses &lt;code&gt;no_std&lt;/code&gt; for WASM. The API server uses &lt;code&gt;std&lt;/code&gt; for native. Same types, both sides. The compiler guarantees they match. No serialization mismatches, no type drift between your API and your contract.&lt;/p&gt;

&lt;p&gt;One language across the entire stack. The chain boundary becomes a function call, not a language barrier.&lt;/p&gt;

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

&lt;p&gt;The first objection any backend engineer raises: what's the overhead?&lt;/p&gt;

&lt;p&gt;Fair. Onchain computation has historically been expensive. That's the whole reason Solidity exists. It was designed to minimize execution cost on the EVM.&lt;/p&gt;

&lt;p&gt;Stylus changes the math. WASM execution on Arbitrum is dramatically cheaper than EVM execution for compute-heavy workloads. The scoring engine used over 90% less gas than the equivalent Solidity contract doing identical work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Gas Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offchain Rust&lt;/td&gt;
&lt;td&gt;953&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onchain Stylus (WASM)&lt;/td&gt;
&lt;td&gt;953&lt;/td&gt;
&lt;td&gt;76,048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onchain Solidity (EVM)&lt;/td&gt;
&lt;td&gt;810&lt;/td&gt;
&lt;td&gt;1,027,635&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Storage operations cost the same either way, but iterative math, weighted calculations, and loop-heavy logic is where WASM pulls ahead.&lt;/p&gt;

&lt;p&gt;This isn't a contrived benchmark. &lt;a href="https://redstone.finance/" rel="noopener noreferrer"&gt;RedStone&lt;/a&gt;, an oracle provider, &lt;a href="https://blog.arbitrum.io/how-redstone-is-advancing-oracle-capabilities-with-stylus" rel="noopener noreferrer"&gt;published similar results&lt;/a&gt; after porting their verification logic to Stylus. Compute-intensive work is where this architecture pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where auditable code matters
&lt;/h2&gt;

&lt;p&gt;Not every function belongs onchain. But some backend logic sits in a category where "trust me" isn't good enough:&lt;/p&gt;

&lt;p&gt;Scoring and risk calculations, where regulators want to audit the exact computation, not a description of it. Eligibility determinations, where partners want to verify outcomes independently instead of trusting your API response. Compliance validation, where auditors want proof that the logic running today is the same logic approved last quarter. Data pipelines, where counterparties want to confirm their data passed through agreed-upon rules.&lt;/p&gt;

&lt;p&gt;If you've ever written documentation explaining how your algorithm works so someone else could trust it, that's the function that belongs onchain. Let them verify the code instead of reading your summary of the code.&lt;/p&gt;

&lt;p&gt;The scoring engine I built is one example of this pattern. But the pattern is the point, not the example. Any pure function where independent verification adds value is a candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'd actually do on a Monday morning
&lt;/h2&gt;

&lt;p&gt;If you write Rust, the learning curve is smaller than you think.&lt;/p&gt;

&lt;p&gt;Install &lt;a href="https://github.com/OffchainLabs/cargo-stylus" rel="noopener noreferrer"&gt;&lt;code&gt;cargo-stylus&lt;/code&gt;&lt;/a&gt;, which handles compilation and deployment. Write your function with &lt;code&gt;#[entrypoint]&lt;/code&gt; and &lt;code&gt;#[public]&lt;/code&gt; macros. Compile to WASM. Deploy to a testnet. Call it from your existing API using an RPC endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[entrypoint]&lt;/span&gt;
&lt;span class="nd"&gt;#[storage]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ScoringEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[public]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ScoringEngine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Factor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Your computation logic here&lt;/span&gt;
        &lt;span class="c1"&gt;// Publicly verifiable, deterministic, immutable&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;Your database, your auth, your business logic, none of that moves. You add one function that runs somewhere verifiable instead of somewhere trusted. Everything else stays the same.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine" rel="noopener noreferrer"&gt;scoring engine repo&lt;/a&gt; has working code, setup instructions, and benchmarks comparing three execution paths: onchain Stylus, onchain Solidity, and offchain Rust. It's a concrete starting point, but the approach applies anywhere you need auditable computation.&lt;/p&gt;
&lt;h2&gt;
  
  
  You're adding a deployment target, not adopting a lifestyle
&lt;/h2&gt;

&lt;p&gt;The shift is smaller than it sounds. The same way you might deploy a function to Lambda for serverless execution or to a TEE for confidential computing, you deploy to Arbitrum for verifiable execution.&lt;/p&gt;

&lt;p&gt;Your Rust skills transfer directly. Your toolchain stays the same. The only thing that changes is who can verify your logic: everyone.&lt;/p&gt;

&lt;p&gt;If you've been dismissing onchain as irrelevant to backend work, spend thirty minutes with the &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/hummusonrails" rel="noopener noreferrer"&gt;
        hummusonrails
      &lt;/a&gt; / &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine" rel="noopener noreferrer"&gt;
        stylus-scoring-engine
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Verifiable credit/risk scoring engine: Stylus (Rust/WASM) vs Solidity on Arbitrum with three-way gas benchmark
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/hummusonrails/stylus-scoring-engine/.github/banner.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhummusonrails%2Fstylus-scoring-engine%2FHEAD%2F.github%2Fbanner.svg" alt="stylus-scoring-engine" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;
  &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/942e017bf0672002dd32a857c95d66f28c5900ab541838c6c664442516309c8a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e7376673f7374796c653d666c61742d737175617265" alt="License"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c609e2b41de6e411f523c403fb94432207ee58321549a51ad8398529273854eb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f727573742d312e38382d6f72616e67652e7376673f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/c609e2b41de6e411f523c403fb94432207ee58321549a51ad8398529273854eb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f727573742d312e38382d6f72616e67652e7376673f7374796c653d666c61742d737175617265" alt="Rust"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/47a6086701dfe590e01cab365b8e6a5ef4fb5fbef9696ace98b09ee0d2133304/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374796c75732d2d73646b2d302e31302e302d3132414146462e7376673f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/47a6086701dfe590e01cab365b8e6a5ef4fb5fbef9696ace98b09ee0d2133304/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374796c75732d2d73646b2d302e31302e302d3132414146462e7376673f7374796c653d666c61742d737175617265" alt="Stylus SDK"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d49b7b9955764e444eb46dfd7f8e2fd481ec8fba30e9b7578e8d7a6ebb1fb480/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6178756d2d302e382d707572706c652e7376673f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/d49b7b9955764e444eb46dfd7f8e2fd481ec8fba30e9b7578e8d7a6ebb1fb480/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6178756d2d302e382d707572706c652e7376673f7374796c653d666c61742d737175617265" alt="Axum"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine/issues" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/25b3e6d0d42c98de74a98cbb4d149a1c09020cf6d1361993b72d7d5b8ffed363/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;Verifiable credit/risk scoring on Arbitrum with a three-way gas benchmark: offchain Rust vs onchain Stylus vs onchain Solidity.&lt;/strong&gt;
  &lt;br&gt;
  &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine#quick-start" rel="noopener noreferrer"&gt;Quick Start&lt;/a&gt; · &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine#architecture" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/hummusonrails/stylus-scoring-engine/issues" rel="noopener noreferrer"&gt;Report a Bug&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benchmarks&lt;/strong&gt; the same scoring algorithm across three execution environments with real gas numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores&lt;/strong&gt; entities against configurable weighted rules with iterative convergence and cross-factor correlation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demonstrates&lt;/strong&gt; Stylus gas savings on compute-heavy workloads (over 90% cheaper than Solidity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shares&lt;/strong&gt; Rust types between the onchain contract and offchain API server from a single crate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stores&lt;/strong&gt; scoring rules in SQLite with full CRUD via REST endpoints&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/hummusonrails/stylus-scoring-engine/.github/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fhummusonrails%2Fstylus-scoring-engine%2FHEAD%2F.github%2Fdemo.gif" alt="Deploy and benchmark demo" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;



&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; clone and install&lt;/span&gt;
git clone https://github.com/hummusonrails/stylus-scoring-engine.git
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; stylus-scoring-engine
pnpm install

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; start the local arbitrum devnode&lt;/span&gt;
pnpm devnode

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; in a new terminal, deploy both contracts&lt;/span&gt;
pnpm deploy:all

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; start the api server (reads .env written by deploy)&lt;/span&gt;
pnpm api

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; in a new terminal, run the three-way benchmark&lt;/span&gt;
pnpm benchmark&lt;/pre&gt;

&lt;/div&gt;

&lt;strong&gt;Prerequisites&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rustup.rs/" rel="nofollow noopener noreferrer"&gt;Rust&lt;/a&gt; 1.88+ with…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/hummusonrails/stylus-scoring-engine" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;The gap between "interesting in theory" and "I could actually use this" is shorter than you expect.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webassembly</category>
      <category>blockchain</category>
      <category>backend</category>
    </item>
    <item>
      <title>The readability scores your content tool is missing</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Tue, 31 Mar 2026 06:00:28 +0000</pubDate>
      <link>https://dev.to/bengreenberg/the-readability-scores-your-content-tool-is-missing-39be</link>
      <guid>https://dev.to/bengreenberg/the-readability-scores-your-content-tool-is-missing-39be</guid>
      <description>&lt;h1&gt;
  
  
  The Readability Scores Your Content Tool Is Missing
&lt;/h1&gt;

&lt;p&gt;Most readability tooling stops at a single score. That is a problem if you are building a documentation pipeline, a content linter, or any system that needs to catch unreadable text before it ships.&lt;/p&gt;

&lt;p&gt;Here are the four metrics worth tracking, what each one actually measures, and what your targets should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flesch-Kincaid Grade Level
&lt;/h2&gt;

&lt;p&gt;This score maps text to a U.S. school grade level based on two inputs: average sentence length and average word length in syllables. A score of 8 means a typical 13-year-old can read it without friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target: 6 to 9 for most technical docs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your score is above 12, sentences are too long or you are leaning on polysyllabic jargon. Users will skim past dense paragraphs instead of reading them. If your score is below 5, you are likely oversimplifying to the point where context is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flesch Reading Ease
&lt;/h2&gt;

&lt;p&gt;This uses the same inputs as FK Grade Level but outputs an inverse score on a 0 to 100 scale. Higher means easier. It weights sentence length more heavily than syllable count, so it punishes run-on sentences hard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target: 60 to 70 for technical documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below 50 and you are in academic or legal territory. Most readers will not finish the section. Above 75 and you may be losing precision, which matters in technical writing where exact phrasing carries meaning. The 60 to 70 range is the practical sweet spot where clarity and accuracy coexist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Readability Index (ARI)
&lt;/h2&gt;

&lt;p&gt;ARI takes a different approach. Instead of counting syllables, it counts characters per word. This makes it faster to compute and less sensitive to syllabification edge cases. It outputs a grade-level score similar to FK but often diverges on technical content, where long words are common but not necessarily difficult for the target audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target: 7 to 10 for developer docs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where ARI earns its place is as a cross-check. If FK says grade 8 but ARI says grade 14, you likely have a cluster of long technical terms inflating the character count. That is worth reviewing even if the content reads fine to a subject matter expert, because new users will not have that context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sentence Length Variance
&lt;/h2&gt;

&lt;p&gt;Most tools report average sentence length and stop there. Variance is the signal they miss. Text where every sentence is roughly the same length reads as monotonous and is harder to parse. Alternating short and long sentences creates rhythm, which keeps readers oriented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target: Standard deviation of 8 to 15 words across your sentence lengths.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below 5 means your writing is flat. Above 20 means sentence structure is inconsistent in a way that will confuse automated parsers and human readers alike. This is especially relevant in procedural docs where scannable short sentences should anchor longer explanatory ones.&lt;/p&gt;




&lt;p&gt;I built a TextAnalytics API that returns all of these scores in a single call -- useful if you are building a linter, a CMS plugin, or just want to quality-gate your content pipeline: &lt;a href="https://rapidapi.com/ben-eI6jno4PU/api/textanalytics" rel="noopener noreferrer"&gt;https://rapidapi.com/ben-eI6jno4PU/api/textanalytics&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>writing</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Quick tip: Flesch-Kincaid Grade Level 8 is your target</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Fri, 27 Mar 2026 11:00:05 +0000</pubDate>
      <link>https://dev.to/bengreenberg/quick-tip-flesch-kincaid-grade-level-8-is-your-target-ci7</link>
      <guid>https://dev.to/bengreenberg/quick-tip-flesch-kincaid-grade-level-8-is-your-target-ci7</guid>
      <description>&lt;p&gt;Quick tip on writing docs and content:&lt;/p&gt;

&lt;p&gt;Flesch-Kincaid Grade Level 8 = readable by a 13-year-old. That's your target for most technical documentation.&lt;/p&gt;

&lt;p&gt;Not because your readers are 13 — but because shorter sentences and common words reduce cognitive load. Even senior engineers read faster at grade 8.&lt;/p&gt;

&lt;p&gt;Most enterprise docs sit at grade 14-16. That's why people skim them.&lt;/p&gt;

&lt;p&gt;The TextAnalytics API returns FK grade level (plus 4 other readability scores) in a single call: &lt;a href="https://rapidapi.com/ben-eI6jno4PU/api/textanalytics" rel="noopener noreferrer"&gt;https://rapidapi.com/ben-eI6jno4PU/api/textanalytics&lt;/a&gt;&lt;/p&gt;

</description>
      <category>writing</category>
      <category>productivity</category>
      <category>devtips</category>
      <category>api</category>
    </item>
    <item>
      <title>I built a link preview API — here's what I learned about Open Graph</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Tue, 24 Mar 2026 07:01:41 +0000</pubDate>
      <link>https://dev.to/bengreenberg/i-built-a-link-preview-api-heres-what-i-learned-about-open-graph-2j99</link>
      <guid>https://dev.to/bengreenberg/i-built-a-link-preview-api-heres-what-i-learned-about-open-graph-2j99</guid>
      <description>&lt;h2&gt;
  
  
  I Built a Link Preview API — Here's What I Learned About Open Graph
&lt;/h2&gt;

&lt;p&gt;Link previews seem simple until you actually build something that generates them reliably. I spent weeks digging into how platforms parse Open Graph metadata, and I kept running into the same category of problems: missing images, wrong fallbacks, cached bad data. Here is what surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Link Previews Actually Are (And Why They Break)
&lt;/h2&gt;

&lt;p&gt;When you paste a URL into Slack or Twitter, the platform fetches that page, reads the &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, and renders a card. The Open Graph protocol, originally developed by Facebook, defines the standard tags most platforms follow: &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, and &lt;code&gt;og:url&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reason previews break so often comes down to a few recurring patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;og:image&lt;/code&gt; tag is missing entirely&lt;/li&gt;
&lt;li&gt;The image URL is relative instead of absolute&lt;/li&gt;
&lt;li&gt;The image dimensions are wrong and the platform rejects it silently&lt;/li&gt;
&lt;li&gt;The metadata exists but the page blocks crawlers with a bad &lt;code&gt;robots.txt&lt;/code&gt; or a JavaScript render wall&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one is brutal. If your content is rendered client-side and you have no SSR or prerendering, most crawlers will fetch an empty shell and your preview will be blank.&lt;/p&gt;

&lt;h2&gt;
  
  
  The og:image Requirements Most Devs Skip
&lt;/h2&gt;

&lt;p&gt;I see this constantly. Developers add an &lt;code&gt;og:image&lt;/code&gt; tag, the preview still looks broken, and they cannot figure out why. The requirements are stricter than the documentation implies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dimensions should be 1200x630 pixels.&lt;/strong&gt; Facebook and LinkedIn will downscale, but if you go too small (under 200x200) they ignore the image completely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size should stay under 1MB.&lt;/strong&gt; Larger images may time out during the crawl window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The URL must be absolute.&lt;/strong&gt; &lt;code&gt;/images/preview.png&lt;/code&gt; will not work. &lt;code&gt;https://yourdomain.com/images/preview.png&lt;/code&gt; will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SVGs.&lt;/strong&gt; Almost no platform will render an SVG as a preview image. Use PNG or JPEG.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Twitter Cards Differ From Open Graph
&lt;/h2&gt;

&lt;p&gt;Twitter does read OG tags as a fallback, but it has its own system called Twitter Cards, and the &lt;code&gt;twitter:card&lt;/code&gt; type changes how everything renders.&lt;/p&gt;

&lt;p&gt;The two types you actually care about are &lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;summary_large_image&lt;/code&gt;. If you use &lt;code&gt;summary&lt;/code&gt;, Twitter shows a small thumbnail beside the text. If you use &lt;code&gt;summary_large_image&lt;/code&gt;, you get the full-width banner image. Most developers want the large image but forget to set the tag, so they get the small thumbnail and wonder why it looks wrong compared to LinkedIn.&lt;/p&gt;

&lt;p&gt;You also need &lt;code&gt;twitter:title&lt;/code&gt; and &lt;code&gt;twitter:description&lt;/code&gt; even if you already have the OG equivalents. Twitter will use OG as a fallback, but being explicit is more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The og:description Fallback Chain
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;og:description&lt;/code&gt; is missing, platforms do not just leave it blank. The fallback chain typically goes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;og:description&lt;/code&gt; meta tag&lt;/li&gt;
&lt;li&gt;Standard &lt;code&gt;meta name="description"&lt;/code&gt; tag&lt;/li&gt;
&lt;li&gt;First paragraph of visible body text&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Knowing this means you can usually ensure something reasonable shows up even on pages that have not been fully optimized.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Gotcha That Bit Me Hard
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;og:url&lt;/code&gt; does not match the canonical URL of the page, platforms will cache the preview against the wrong URL. I had a staging URL leak into production metadata once and spent an embarrassing amount of time wondering why the preview was showing old content. Always set &lt;code&gt;og:url&lt;/code&gt; explicitly to the canonical version.&lt;/p&gt;




&lt;p&gt;Parsing all of this reliably across different platforms, redirect chains, and malformed HTML is exactly the kind of work that sounds quick and turns into a week of edge cases. I use what I built in production at LinkPreview API. It handles the messy parts of parsing OG data so you do not have to: &lt;a href="https://rapidapi.com/ben-eI6jno4PU/api/linkpreview1" rel="noopener noreferrer"&gt;https://rapidapi.com/ben-eI6jno4PU/api/linkpreview1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>5 things your website is getting wrong (and how to check for free)</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Sun, 22 Mar 2026 21:51:15 +0000</pubDate>
      <link>https://dev.to/bengreenberg/5-things-your-website-is-getting-wrong-and-how-to-check-for-free-22fm</link>
      <guid>https://dev.to/bengreenberg/5-things-your-website-is-getting-wrong-and-how-to-check-for-free-22fm</guid>
      <description>&lt;p&gt;Most websites fail basic technical hygiene checks. Not because developers don't care, but because these things are easy to miss when you're focused on shipping features. Here are five common issues worth fixing today.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Missing or Wrong Security Headers
&lt;/h2&gt;

&lt;p&gt;Headers like &lt;code&gt;Content-Security-Policy&lt;/code&gt;, &lt;code&gt;X-Frame-Options&lt;/code&gt;, and &lt;code&gt;Strict-Transport-Security&lt;/code&gt; (HSTS) protect your users from clickjacking, XSS attacks, and protocol downgrade attacks. Skipping them leaves real attack surface open. Browsers and security scanners will flag these absences, and some enterprise clients actively check before integrating with your API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; Run &lt;code&gt;curl -I https://yourdomain.com&lt;/code&gt; and scan the response headers. Or paste your URL into &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt; for a free graded report.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Open Graph Tags That Break Link Previews
&lt;/h2&gt;

&lt;p&gt;When someone shares your link on Slack, LinkedIn, or Twitter, the platform reads your Open Graph meta tags to build the preview card. If &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, or &lt;code&gt;og:description&lt;/code&gt; are missing or misconfigured, the preview looks broken or empty. This tanks click-through rates on content you spent real time creating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; Use the &lt;a href="https://www.opengraph.xyz" rel="noopener noreferrer"&gt;OpenGraph.xyz&lt;/a&gt; previewer. Paste your URL and see exactly what Slack or LinkedIn will render. Fix any missing tags in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. No Canonical URL
&lt;/h2&gt;

&lt;p&gt;If your page is reachable at both &lt;code&gt;https://example.com/page&lt;/code&gt; and &lt;code&gt;https://example.com/page?ref=newsletter&lt;/code&gt;, search engines may treat these as separate pages competing against each other. Over time this splits your ranking signals and can suppress both versions. A canonical tag tells crawlers which URL is the one that counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; Open DevTools in Chrome, go to Elements, and search for &lt;code&gt;canonical&lt;/code&gt; in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. You should find something like &lt;code&gt;&amp;lt;link rel="canonical" href="https://example.com/page" /&amp;gt;&lt;/code&gt;. If it is missing or pointing to the wrong URL, fix it in your template.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Images Without Alt Text
&lt;/h2&gt;

&lt;p&gt;Alt text is not optional. Screen readers rely on it for users with visual impairments, and search engine crawlers use it to understand image content. A page full of images with empty or missing &lt;code&gt;alt&lt;/code&gt; attributes is both an accessibility failure and a missed SEO opportunity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; Run &lt;code&gt;document.querySelectorAll('img:not([alt])')&lt;/code&gt; in the browser console. Any results mean you have untagged images. Axe DevTools (free browser extension) will also flag these automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Missing Viewport Meta Tag
&lt;/h2&gt;

&lt;p&gt;Without &lt;code&gt;&amp;lt;meta name="viewport" content="width=device-width, initial-scale=1"&amp;gt;&lt;/code&gt; in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, mobile browsers will render your page at desktop width and then scale it down. The result is a tiny, unreadable layout that frustrates users and tanks your Core Web Vitals scores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check:&lt;/strong&gt; Open DevTools, toggle the device toolbar (Ctrl+Shift+M), and load your page. If it renders like a shrunken desktop site, the viewport tag is probably missing. Check the Elements panel to confirm.&lt;/p&gt;




&lt;p&gt;These five checks take maybe 15 minutes to run manually. If you want to do all of them in one go, I built a free tool that runs 5 key checks instantly, no signup, no waiting: &lt;a href="https://audit.hummusonrails.com/free" rel="noopener noreferrer"&gt;https://audit.hummusonrails.com/free&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Quick tip: your og:image should be 1200x630px</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Fri, 20 Mar 2026 12:00:10 +0000</pubDate>
      <link>https://dev.to/bengreenberg/quick-tip-your-ogimage-should-be-1200x630px-4m27</link>
      <guid>https://dev.to/bengreenberg/quick-tip-your-ogimage-should-be-1200x630px-4m27</guid>
      <description>&lt;p&gt;Quick tip about link previews:&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;og:image&lt;/code&gt; should be &lt;strong&gt;1200x630px&lt;/strong&gt;. Most sites get this wrong — either wrong dimensions, an SVG (which many platforms reject), or a relative URL instead of absolute.&lt;/p&gt;

&lt;p&gt;Check yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yoursite.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'og:image'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building something that reads og:image from other sites, the LinkPreview API handles all the edge cases: &lt;a href="https://rapidapi.com/ben-eI6jno4PU/api/linkpreview1" rel="noopener noreferrer"&gt;https://rapidapi.com/ben-eI6jno4PU/api/linkpreview1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>devtips</category>
      <category>opengraph</category>
    </item>
    <item>
      <title>Quick tip: check your security headers with curl</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Wed, 18 Mar 2026 21:51:11 +0000</pubDate>
      <link>https://dev.to/bengreenberg/quick-tip-check-your-security-headers-with-curl-1n94</link>
      <guid>https://dev.to/bengreenberg/quick-tip-check-your-security-headers-with-curl-1n94</guid>
      <description>&lt;p&gt;Quick tip for checking your security headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://yoursite.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'x-frame\|content-security\|strict-transport\|x-content-type'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get no output, you're missing all of them. HSTS is the most important one to add first — it forces HTTPS on all future visits.&lt;/p&gt;

&lt;p&gt;Want a full breakdown? Run a free check at &lt;a href="https://audit.hummusonrails.com/free" rel="noopener noreferrer"&gt;https://audit.hummusonrails.com/free&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>devtips</category>
      <category>cli</category>
    </item>
    <item>
      <title>I got tired of building the same link preview function, so I made it an API</title>
      <dc:creator>Ben Greenberg</dc:creator>
      <pubDate>Sun, 15 Mar 2026 18:01:45 +0000</pubDate>
      <link>https://dev.to/bengreenberg/i-got-tired-of-building-the-same-link-preview-function-so-i-made-it-an-api-47bg</link>
      <guid>https://dev.to/bengreenberg/i-got-tired-of-building-the-same-link-preview-function-so-i-made-it-an-api-47bg</guid>
      <description>&lt;p&gt;Every app I've built in the last few years has needed the same thing: paste a URL, show a preview card. Slack does it. Discord does it. Every CMS does it. And every time, I end up writing the same cheerio scraping code, handling the same edge cases with Open Graph tags, and debugging the same issue where Twitter Cards use &lt;code&gt;name&lt;/code&gt; instead of &lt;code&gt;property&lt;/code&gt; and half the internet gets it wrong.&lt;/p&gt;

&lt;p&gt;A little while ago I finally extracted all of that into a standalone API and put it on RapidAPI. Figured other people are writing the same code too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually returns
&lt;/h2&gt;

&lt;p&gt;You pass a URL. You get back structured metadata across six layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open Graph (title, description, images with dimensions, article metadata)&lt;/li&gt;
&lt;li&gt;Twitter Cards (card type, site, creator; only tags that are actually present, no fallback guessing)&lt;/li&gt;
&lt;li&gt;HTML meta (title tag, meta description, canonical, theme color)&lt;/li&gt;
&lt;li&gt;Icons (auto-selects the highest quality favicon with a priority chain)&lt;/li&gt;
&lt;li&gt;Feeds (discovers RSS, Atom, and JSON Feed links)&lt;/li&gt;
&lt;li&gt;JSON-LD (parses all script blocks, prefers Article/Product over BreadcrumbList)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response gives you both a merged top-level view (where the title comes from whichever source has it; OG first, then Twitter, then the title tag) and the raw parsed layers, so you can do your own logic.&lt;/p&gt;

&lt;p&gt;Here's what GitHub returns:&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHub · Build and ship software on a single, collaborative platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Join the world's most widely adopted..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&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://github.githubassets.com/images/modules/site/social-cards/campaign-social.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;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;630&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;"favicon"&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://github.githubassets.com/favicons/favicon.svg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"siteName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHub"&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;"website"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"themeColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#1e2327"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openGraph"&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="err"&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;"twitter"&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="err"&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;"feeds"&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;"jsonLd"&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;"@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;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;234&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The parts that were annoying to get right
&lt;/h2&gt;

&lt;p&gt;A few things I ran into while building this that I suspect anyone writing their own scraper will hit too:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OG tags use &lt;code&gt;property&lt;/code&gt;, Twitter uses &lt;code&gt;name&lt;/code&gt;.&lt;/strong&gt; The Open Graph spec says &lt;code&gt;&amp;lt;meta property="og:title"&amp;gt;&lt;/code&gt;. Twitter Cards say &lt;code&gt;&amp;lt;meta name="twitter:card"&amp;gt;&lt;/code&gt;. But a surprising number of sites swap them. The parser checks both attributes for both prefixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple &lt;code&gt;og:image&lt;/code&gt; tags are valid.&lt;/strong&gt; The OG spec supports arrays by repeating the tag. Structured properties like &lt;code&gt;og:image:width&lt;/code&gt; apply to the most recently declared &lt;code&gt;og:image&lt;/code&gt;. Most scrapers just grab the first one and ignore the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON-LD blocks are a mess.&lt;/strong&gt; A typical news article page has three or four JSON-LD blocks: a BreadcrumbList, an Organization, and then the actual Article buried in the third one. You need to parse all of them and pick the right one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Favicons have a priority order.&lt;/strong&gt; Apple touch icons at 180x180 are usually the highest quality. Then standard icons at 32x32, then the generic &lt;code&gt;/favicon.ico&lt;/code&gt; fallback. Most implementations just grab the first &lt;code&gt;&amp;lt;link rel="icon"&amp;gt;&lt;/code&gt; they find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relative URLs everywhere.&lt;/strong&gt; OG images and feed links are often relative paths. You need the effective URL (after redirects) as the base to resolve them correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical approach
&lt;/h2&gt;

&lt;p&gt;It's a Fastify server running on a VPS, using cheerio for HTML parsing. No headless browser, no Puppeteer, just fetch the HTML and parse it. This keeps response times under 500ms for cache misses and under 5ms for cache hits.&lt;/p&gt;

&lt;p&gt;SSRF protection was the part I spent the most time on. Since the API accepts arbitrary URLs from users, you need to prevent people from using it to probe internal networks. The server resolves the hostname first, checks the IP against a blocklist of private ranges, then connects directly to the resolved IP to prevent DNS rebinding attacks.&lt;/p&gt;

&lt;p&gt;I also built a text analytics API while I was at it, using the same infrastructure but for a different purpose. Pass in text, get back readability scores (Flesch-Kincaid, Coleman-Liau, SMOG, and three others), keyword density, bigrams, trigrams, and reading time. All pure math on strings, sub-10ms responses. Useful for content optimization tools and writing assistants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try them
&lt;/h2&gt;

&lt;p&gt;Both APIs are on RapidAPI with a free tier (500 requests/month):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rapidapi.com/ben-eI6jno4PU/api/linkpreview1" rel="noopener noreferrer"&gt;LinkPreview&lt;/a&gt; — URL metadata extraction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rapidapi.com/ben-eI6jno4PU/api/textanalytics" rel="noopener noreferrer"&gt;TextAnalytics&lt;/a&gt; — readability scores, keyword density, text metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free tier is enough to test and prototype with. &lt;/p&gt;

&lt;p&gt;If you run into any edge cases the parser doesn't handle well, I'd genuinely like to know. Parsing the wild HTML of the internet is a forever project.&lt;/p&gt;

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