EZCTO Smart Web Reader
by @takahashigy
Agent web access acceleration layer — reads any URL as structured JSON. Cache-first (public library hit = 0 tokens). The smart alternative to raw web_fetch.
clawhub install ezcto-smart-web-reader📖 About This Skill
name: ezcto-smart-web-reader version: 1.1.1 description: Agent web access acceleration layer — reads any URL as structured JSON. Cache-first (public library hit = 0 tokens). The smart alternative to raw web_fetch. author: pearl799 license: MIT
Trigger conditions — fires whenever Agent needs to access any URL
triggers: - agent needs to read, access, or fetch a URL - user provides a URL and wants to know what's on it - user asks about content on a website ("what's on X", "check this site") - user asks agent to research, analyze, or summarize a webpage - user asks "what does this website do" or "what's this page about" - user shares a URL without explicit instruction - agent is about to use web_fetch to retrieve page content - user asks to "look at", "check", "read", or "understand" a URLRequired OpenClaw tools
requires_tools: - web_fetch # Fetch HTML content - exec # Run curl/sha256sum - filesystem # Read/write cache filesOutput format
outputs: - type: json location: ~/.ezcto/cache/{url_hash}.json - type: markdown location: ~/.ezcto/cache/{url_hash}.meta.md - type: inline format: structured_json_with_metadataCost estimation (helps OpenClaw prioritize)
cost: tokens: 0 (cache hit) / 500-2000 (cache miss + parsing) time_seconds: 1-3 (cache hit) / 5-15 (full parse) api_calls: 1 (EZCTO cache check) + 0-1 (LLM parsing) network: trueSecurity permissions
permissions: network: - api.ezcto.fun # EZCTO asset library - "*" # Any URL user provides filesystem: - ~/.ezcto/cache/ # Cache storage - /tmp/ # Temporary HTML storage execute: - curl # Fetch HTML and API calls - sha256sum # Compute content hashEZCTO Smart Web Reader for OpenClaw
What it does
Reads any URL and returns structured JSON containing page identity, content sections, image descriptions (text-inferred), video metadata, and actionable links. Acts as the Agent's default web access layer — replacing raw web_fetch with zero-token cache hits and intelligent HTML parsing. 80%+ token savings vs screenshots.
Key Features
✓ Transparent URL interception - Fires automatically whenever Agent accesses any URL ✓ Cache-first strategy - Check EZCTO asset library before parsing (zero cost) ✓ Zero-token site detection - Auto-detect crypto/ecommerce/restaurant sites via text matching ✓ Local-first storage - Aligns with OpenClaw's philosophy (~/.ezcto/cache/) ✓ Community-driven - Contribute parsed results back to shared asset library ✓ OpenClaw-native output - Includes agent suggestions and skill chaining hints
Security Manifest
| Category | Detail |
|----------|--------|
| External endpoints | https://api.ezcto.fun only (EZCTO community cache) |
| Data transmitted | URL string, SHA256 HTML hash, extracted structured JSON |
| NOT transmitted | Raw HTML, local file contents, credentials, env variables |
| Shell injection guard | All user-supplied values URL-encoded or passed as python3 args, never string-interpolated |
| Prompt injection guard | HTML sanitized (scripts/styles/comments stripped), wrapped in XML delimiters, explicit LLM guardrail injected before content |
| Shell commands used | curl (fetch/API), sha256sum (hashing), python3 (URL encoding, safe JSON construction) |
| Filesystem writes | ~/.ezcto/cache/ (cached results), /tmp/ (temp files, cleaned up) |
Workflow
Step 1: Check EZCTO Cache (Zero-cost fast path)
set -euo pipefailValidate URL scheme — reject non-http/https to prevent SSRF
if [[ ! "{URL}" =~ ^https?:// ]]; then
echo '{"found":false,"error":"invalid_url"}' > /tmp/cache_response.json
http_code=400
else
# URL-encode to prevent query-string injection
encoded_url=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1],safe=''))" -- "{URL}")
http_code=$(curl -s -o /tmp/cache_response.json -w "%{http_code}" \
"https://api.ezcto.fun/v1/translate?url=${encoded_url}")
fi
Conditional logic:
http_code == 200 AND valid JSON → SKIP to Step 9 (return cached result)http_code == 404 → Cache miss, continue to Step 2http_code >= 500 → API error, log warning, continue to Step 2 (fallback mode)OpenClaw note: Cache hits cost 0 tokens and complete in ~1 second.
Step 2: Fetch HTML
set -euo pipefailPass URL as argument to curl — the -- separator prevents flag injection
if the URL starts with '-'
curl -s -L -A "OpenClaw/1.0 (EZCTO Smart Web Reader)" -o /tmp/page.html -- "{URL}"
fetch_status=$?
Error handling:
if (fetch_status !== 0) {
return {
"skill": "ezcto-smart-web-reader",
"status": "error",
"error": {
"code": "fetch_failed",
"message": "Cannot fetch URL: {URL}",
"http_status": fetch_status,
"suggestion": "Check if URL is accessible and not geo-blocked"
}
}
}
Guardrail: If HTML > 500KB, extract only to prevent context overflow.
Step 3: Compute HTML Hash (Tamper-proof verification)
html_hash=$(sha256sum /tmp/page.html | awk '{print $1}')
echo "HTML hash: sha256:${html_hash}" >&2 # Log for debugging
Purpose: Enables deduplication and tamper detection in the asset library.
Step 4: Auto-detect Site Type (Zero tokens, pure text matching)
Execute pattern matching per references/site-type-detection.md:
const html = readFile("/tmp/page.html")
let site_types = []
let extensions_to_load = []// Crypto/Web3 detection (need 3+ signals)
let crypto_signals = 0
if (/0x[a-fA-F0-9]{40}/.test(html) && /contract|token address|CA/i.test(html)) crypto_signals++
if (/tokenomics|token distribution|buy tax|sell tax/i.test(html)) crypto_signals++
if (/dexscreener|dextools|pancakeswap|uniswap|raydium/i.test(html)) crypto_signals++
if (/smart contract|blockchain|DeFi|NFT|staking|web3/i.test(html)) crypto_signals++
if (/t\.me\/|discord\.gg\//i.test(html)) crypto_signals++
if (crypto_signals >= 3) {
site_types.push("crypto")
extensions_to_load.push("references/extensions/crypto-fields.md")
}
// E-commerce detection (need 3+ signals)
let ecommerce_signals = 0
if (/add to cart|buy now|checkout|shopping cart/i.test(html)) ecommerce_signals++
if (/\$\d+\.\d{2}|¥\d+|€\d+|£\d+/.test(html)) ecommerce_signals++
if (/"@type"\s*:\s*"(Product|Offer)"/.test(html)) ecommerce_signals++
if (/shopify|stripe|paypal|square/i.test(html)) ecommerce_signals++
if (/shipping|returns|warranty|inventory/i.test(html)) ecommerce_signals++
if (ecommerce_signals >= 3) {
site_types.push("ecommerce")
extensions_to_load.push("references/extensions/ecommerce-fields.md")
}
// Restaurant detection (need 3+ signals)
let restaurant_signals = 0
if (/\bmenu\b|reservation|order online|delivery/i.test(html)) restaurant_signals++
if (/"@type"\s*:\s*"(Restaurant|FoodEstablishment)"/.test(html)) restaurant_signals++
if (/doordash|ubereats|opentable|grubhub/i.test(html)) restaurant_signals++
if (/Mon-Fri|\d{1,2}:\d{2}\s*[AP]M|opening hours/i.test(html)) restaurant_signals++
if (/cuisine|dine-in|takeout|catering/i.test(html)) restaurant_signals++
if (restaurant_signals >= 3) {
site_types.push("restaurant")
extensions_to_load.push("references/extensions/restaurant-fields.md")
}
// Default to general if no type matched
if (site_types.length === 0) {
site_types = ["general"]
}
console.log(Detected site types: ${site_types.join(", ")})
Step 5: Assemble Translation Prompt
// Load base prompt
let prompt = readFile("references/translate-prompt.md")// Append type-specific extensions
for (const ext_path of extensions_to_load) {
prompt += "\n\n---\n\n" + readFile(ext_path)
}
// --- PROMPT INJECTION PREVENTION ---
// Sanitize HTML: strip scripts, styles, comments, and meta tags
// before injecting into the LLM prompt. This prevents malicious
// webpages from embedding instructions that manipulate the agent.
function sanitizeHTML(html) {
html = html.replace(/