{"id":"ac725540-0ebd-42b3-a693-2d1150ce2071","entityType":"agent","slug":"clawhub-chrischall-otc-curl","name":"otc-curl","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-otc-curl","canonicalPath":"/agent/clawhub-chrischall-otc-curl","generatedAt":"2026-10-11T03:55:36.312Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T00:17:50.983Z","emptyReason":null},"description":"Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available. Skill: otc-curl Owner: chrischall Summary: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available. Tags: latest:1.1.7 Version hi","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.2K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:otc-curl","sourceUrl":"https://clawhub.ai/chrischall/otc-curl","homepage":"https://clawhub.ai/chrischall/skills/otc-curl","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/otc-curl","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/otc-curl","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event l"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T00:17:50.983Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T00:17:50.983Z","emptyReason":null},"stars":null,"forks":null,"downloads":1221,"packageName":null,"latestVersion":"1.1.7","tractionLabel":"1.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T00:17:50.882Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T00:17:50.983Z","lastCrawledAt":"2026-10-11T00:17:50.882Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T00:17:50.882Z","lastVerifiedAt":null,"highlights":[{"version":"1.1.7","createdAt":"2026-10-09T23:28:46.243Z","changelog":"- Removed the sample file: skill-card.md. - No user-facing or functional changes; documentation and skill usage remain unchanged.","fileCount":4,"zipByteSize":6469},{"version":"1.1.6","createdAt":"2026-10-07T13:35:39.420Z","changelog":"- Removed the file skill-card.md. - No changes to skill functionality or documentation.","fileCount":4,"zipByteSize":6478},{"version":"1.1.5","createdAt":"2026-10-05T02:55:19.403Z","changelog":"- Removed the skill-card.md file for this version. - No other user-facing changes; functionality and documentation are unchanged.","fileCount":4,"zipByteSize":6462},{"version":"1.1.4","createdAt":"2026-10-03T01:43:26.876Z","changelog":"- Removed the skill-card.md file. - No changes to functionality or usage; documentation remains as before.","fileCount":4,"zipByteSize":6485},{"version":"1.1.3","createdAt":"2026-09-25T15:55:55.588Z","changelog":"- Removed the file skill-card.md. - No user-facing changes to core documentation or functionality.","fileCount":4,"zipByteSize":6511},{"version":"1.1.2","createdAt":"2026-09-23T21:39:02.607Z","changelog":"- Removed the file skill-card.md. - No changes to core functionality or documentation.","fileCount":4,"zipByteSize":6744},{"version":"1.1.1","createdAt":"2026-09-23T15:40:19.676Z","changelog":"- Removed the file: skill-card.md. - No changes to functionality or documentation aside from file removal.","fileCount":4,"zipByteSize":6790},{"version":"1.1.0","createdAt":"2026-09-20T02:52:14.210Z","changelog":"- Removed the file: skill-card.md. - No changes to functionality or usage. - Documentation and skill behavior remain unchanged.","fileCount":4,"zipByteSize":6772}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:otc-curl","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T03:55:36.309Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-otc-curl/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T00:17:50.983Z","emptyReason":null},"readme":"Skill: otc-curl\n\nOwner: chrischall\n\nSummary: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n\nTags: latest:1.1.7\n\nVersion history:\n\nv1.1.7 | 2026-10-09T23:28:46.243Z | auto\n\n- Removed the sample file: skill-card.md.\n- No user-facing or functional changes; documentation and skill usage remain unchanged.\n\nv1.1.6 | 2026-10-07T13:35:39.420Z | auto\n\n- Removed the file skill-card.md.\n- No changes to skill functionality or documentation.\n\nv1.1.5 | 2026-10-05T02:55:19.403Z | auto\n\n- Removed the skill-card.md file for this version.\n- No other user-facing changes; functionality and documentation are unchanged.\n\nv1.1.4 | 2026-10-03T01:43:26.876Z | auto\n\n- Removed the skill-card.md file.\n- No changes to functionality or usage; documentation remains as before.\n\nv1.1.3 | 2026-09-25T15:55:55.588Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing changes to core documentation or functionality.\n\nv1.1.2 | 2026-09-23T21:39:02.607Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core functionality or documentation.\n\nv1.1.1 | 2026-09-23T15:40:19.676Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to functionality or documentation aside from file removal.\n\nv1.1.0 | 2026-09-20T02:52:14.210Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to functionality or usage.\n- Documentation and skill behavior remain unchanged.\n\nv1.0.0 | 2026-09-19T11:19:01.445Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to functionality; documentation only.\n\nv0.4.3 | 2026-09-14T14:14:53.281Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation; this version only deletes an unused file.\n\nv0.4.2 | 2026-09-10T17:52:39.390Z | auto\n\n- Removed the file: skill-card.md\n- No changes to skill features or usage. This is a documentation cleanup update.\n\nv0.4.1 | 2026-09-05T00:56:54.665Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to functionality or documentation content.\n\nv0.4.0 | 2026-09-04T22:23:49.597Z | auto\n\n- Removed the skill-card.md file.\n- No changes to commands or usage; documentation and core functionality remain the same.\n\nv0.3.3 | 2026-08-09T21:03:05.528Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to functionality or documentation in SKILL.md.\n- This update is a cleanup release with no user-facing feature changes.\n\nv0.3.2 | 2026-07-27T21:16:15.806Z | auto\n\n- Improved documentation, including detailed usage instructions and best practices.\n- Clarified how to select city base URLs and properly filter out expired deals.\n- Warned about unique site-specific pitfalls (category ids, date formats, event page truncation).\n- Added more example curl/jq commands and troubleshooting tips for accurate event and article querying.\n\nArchive index:\n\nArchive v1.1.7: 4 files, 6469 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (1787b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.7:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.7\",\n  \"publishedAt\": 1791588526243\n}\n\nFile v1.1.7:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.7:skill-card.md\n\n## Description:\n\nFind local events and inexpensive activities on On the Cheap city sites using public web pages and WordPress APIs when the onthecheap MCP server is unavailable.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople and agents looking for free or inexpensive local events and deals can search On the Cheap city sites without the onthecheap MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Requests may go to an unintended city site.\n\nMitigation: Check the city BASE value before running curl commands.\n\nRisk: Expired deals or incomplete or misdated event listings may mislead users.\n\nMitigation: Resolve the site's expired category, confirm day-page dates, and fetch full day pages instead of treating the month grid as complete.\n\n## Reference(s):\n\n- [otc-curl on ClawHub](https://clawhub.ai/chrischall/skills/otc-curl)\n- [On the Cheap extraction recipes](references/recipes.md)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with bash examples and event or deal summaries]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [City-specific public listings and article links; availability and prices should be checked at the source.]\n\n## Skill Version(s):\n\n1.1.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.6: 4 files, 6478 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (1830b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.6:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.6\",\n  \"publishedAt\": 1791380139420\n}\n\nFile v1.1.6:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.6:skill-card.md\n\n## Description:\n\nHelps agents find local events and deals across On the Cheap city sites using shell queries when the onthecheap MCP server is unavailable.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents and their users can look up free or low-cost local activities, event times, prices and venues, and searchable articles across On the Cheap city sites when the dedicated MCP server is unavailable.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Live website requests may return stale or incomplete event and deal listings.\n\nMitigation: Exclude each site's expired category, verify event dates, and fetch individual day pages for complete schedules.\n\nRisk: Adapting the shell examples to a different site changes where requests are sent.\n\nMitigation: Review the commands and destination before running them, especially when changing BASE.\n\n## Reference(s):\n\n- [otc-curl on ClawHub](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Ready-to-run recipes](references/recipes.md)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Text, Guidance]\n\n**Output Format:** [Markdown with shell examples and event or article listings]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Listings depend on live public city sites; no API key is required.]\n\n## Skill Version(s):\n\n1.1.6 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.5: 4 files, 6462 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (1768b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.5:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1791168919403\n}\n\nFile v1.1.5:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.5:skill-card.md\n\n## Description:\n\nProvides curl and jq recipes to find local events and affordable activities across On the Cheap city sites when the onthecheap MCP server is unavailable.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople and agents seeking free or low-cost local activities use this skill to search articles and retrieve daily event listings from On the Cheap city sites without the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Live requests may reach sites outside the intended On the Cheap network.\n\nMitigation: Keep target URLs within the intended public city sites and review retrieved content.\n\nRisk: Expired deals, incorrectly parsed dates, or truncated month calendars can misrepresent available events.\n\nMitigation: Resolve the site's expired-category ID, verify day-page dates, and use full day pages for complete event listings.\n\n## Reference(s):\n\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Ready-to-run recipes](references/recipes.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Text]\n\n**Output Format:** [Markdown with shell commands and local-event or article summaries]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Results depend on live public website responses.]\n\n## Skill Version(s):\n\n1.1.5 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.4: 4 files, 6485 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (1800b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.4:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.4\",\n  \"publishedAt\": 1790991806876\n}\n\nFile v1.1.4:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.4:skill-card.md\n\n## Description:\n\nHelps agents find local events and affordable activities on On the Cheap city sites using public web pages and WordPress APIs.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople looking for free or inexpensive local activities can use an agent with this skill to find event listings and related articles when the onthecheap MCP server is unavailable.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Results rely on live third-party websites and may be outdated or incomplete.\n\nMitigation: Check the requested date against the page heading, fetch full day listings rather than relying on truncated month views, and verify important details on the source page.\n\nRisk: Article searches can return expired offers.\n\nMitigation: Look up and exclude each site's expired category before showing deals.\n\n## Reference(s):\n\n- [On the Cheap query recipes](references/recipes.md)\n- [otc-curl on ClawHub](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with event and article details, links, and optional shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Results depend on live public website content.]\n\n## Skill Version(s):\n\n1.1.4 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.3: 4 files, 6511 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (1927b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1790351755588\n}\n\nFile v1.1.3:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.3:skill-card.md\n\n## Description:\n\nHelps agents find local events and affordable activities on On the Cheap city sites using public web requests when the dedicated MCP server is unavailable.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents and their users can look up inexpensive local events, deal articles, dates, prices, and venues in participating On the Cheap cities when the dedicated MCP server is unavailable.\n\n### Deployment Geography for Use:\n\nGlobal; listings cover participating U.S. cities.\n\n## Known Risks and Mitigations:\n\nRisk: Public web requests disclose the queried city, date, and search terms to the selected site.\n\nMitigation: Use only public queries and avoid sending credentials or private data.\n\nRisk: Expired articles, silently misread date paths, or truncated month views can produce misleading listings.\n\nMitigation: Resolve the site's expired-category ID, verify the returned day heading, and fetch day pages for complete event listings.\n\n## Reference(s):\n\n- [otc-curl ClawHub release](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Query and extraction recipes](references/recipes.md)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown or plain text with optional shell commands and event listings]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Results depend on public city-site content and the requested city and date.]\n\n## Skill Version(s):\n\n1.1.3 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.2: 4 files, 6744 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (2482b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1790199542607\n}\n\nFile v1.1.2:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.2:skill-card.md\n\n## Description:\n\nQuery the On the Cheap network from the shell with curl and jq for daily local event listings, prices, venues, and searchable articles on free and cheap things to do.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to retrieve public local event and deal information from On the Cheap city sites when the onthecheap MCP server is unavailable. It helps compose curl, jq, and Python snippets for article searches, event-day lookups, calendar counts, and HTML cleanup.\n\n### Deployment Geography for Use:\n\nGlobal, for querying public On the Cheap city sites where access is permitted.\n\n## Known Risks and Mitigations:\n\nRisk: The skill makes network requests to public On the Cheap sites through shown curl snippets.\n\nMitigation: Review commands before running them and use the skill only for public event and deal lookups.\n\nRisk: Fetched article and event text is untrusted web content.\n\nMitigation: Do not provide credentials or private data to the fetched pages, and treat returned text as data to summarize or inspect rather than as instructions.\n\nRisk: Incorrect date formats, stale expired categories, or truncated month views can produce misleading event or deal results.\n\nMitigation: Resolve site-specific category IDs, confirm event-page headings, decode titles before display, and fetch day pages for complete listings.\n\n## Reference(s):\n\n- [Ready-to-run recipes](references/recipes.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n- [Mile High on the Cheap](https://www.milehighonthecheap.com)\n- [Triangle on the Cheap](https://triangleonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Code, Shell commands, Guidance]\n\n**Output Format:** [Markdown guidance with bash, jq, and Python snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires curl and jq, with optional Python snippets for HTML entity decoding and event-page parsing.]\n\n## Skill Version(s):\n\n1.1.2 (source: release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.1: 4 files, 6790 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (2579b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.1:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.1\",\n  \"publishedAt\": 1790178019676\n}\n\nFile v1.1.1:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.1:skill-card.md\n\n## Description:\n\nQuery the On the Cheap network from the shell with curl and jq for daily local event listings with times, prices, and venues, plus searchable articles on free and cheap things to do.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill as a shell-based fallback for finding On the Cheap local event and deal data when the onthecheap MCP server is unavailable. It provides commands and parsing guidance for public WordPress articles and rendered event pages.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill asks the agent to make read-only web requests to public On the Cheap sites and run local parsing commands.\n\nMitigation: Install only when that web access and local use of curl, jq, and python3 are acceptable for the requested lookup.\n\nRisk: Article results can include expired deals if the site-specific expired category is not resolved and excluded.\n\nMitigation: Resolve the expired category ID for the selected site before querying articles, and do not reuse category IDs across sites.\n\nRisk: Event date paths silently accept the wrong format and can return January 1, 1970 data.\n\nMitigation: Convert dates to M-D-YYYY for event URLs and confirm the rendered event heading matches the requested date.\n\nRisk: Monthly calendar pages are truncated and can undercount events.\n\nMitigation: Use month pages to identify busy days, then fetch day pages before presenting a complete schedule.\n\n## Reference(s):\n\n- [Ready-to-run recipes](artifact/references/recipes.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Charlotte On the Cheap](https://www.charlotteonthecheap.com)\n- [Mile High On the Cheap](https://www.milehighonthecheap.com)\n- [Triangle On the Cheap](https://triangleonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with inline bash and Python code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces read-only public web requests and local parsing commands using curl, jq, and python3.]\n\n## Skill Version(s):\n\n1.1.1 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.0: 4 files, 6772 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (2514b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.1.0:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.1.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.0\",\n  \"publishedAt\": 1789872734210\n}\n\nFile v1.1.0:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.1.0:skill-card.md\n\n## Description:\n\nQuery the On the Cheap network from the shell with curl and jq for daily local event listings with times, prices, and venues, plus searchable articles on free and cheap things to do.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill as a fallback when the onthecheap MCP server is unavailable and they need shell-based access to public On the Cheap event and deal information. It helps retrieve articles, daily event listings, and calendar summaries while avoiding stale or truncated results.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill guides agents to run shell and Python snippets that make public web requests.\n\nMitigation: Review generated commands before execution, especially when changing BASE to another site.\n\nRisk: Using an ISO date path for event pages can silently return January 1, 1970 results.\n\nMitigation: Convert event dates to M-D-YYYY and confirm the page heading matches the requested date.\n\nRisk: Expired category IDs and taxonomy IDs differ by site, so reused IDs can return stale or incorrect results.\n\nMitigation: Resolve category and location IDs on the selected site before querying or filtering results.\n\nRisk: Month calendar pages show truncated listings in each day cell.\n\nMitigation: Use month pages for discovery only, then fetch day pages for complete event schedules.\n\n## Reference(s):\n\n- [Ready-to-run recipes](references/recipes.md)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n- [Mile High on the Cheap](https://www.milehighonthecheap.com)\n- [Triangle on the Cheap](https://triangleonthecheap.com)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/otc-curl)\n\n## Skill Output:\n\n**Output Type(s):** [guidance, shell commands, code, markdown]\n\n**Output Format:** [Markdown with inline bash, jq, and Python command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands make public web requests and may require local curl, jq, and Python for parsing or decoding.]\n\n## Skill Version(s):\n\n1.1.0 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.0: 4 files, 6679 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (2298b), SKILL.md (3849b), _meta.json (127b)\n\nFile v1.0.0:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v1.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.0.0\",\n  \"publishedAt\": 1789816741445\n}\n\nFile v1.0.0:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v1.0.0:skill-card.md\n\n## Description:\n\nQuery the On the Cheap network from the shell with curl and jq for daily local event listings with times, prices, and venues, plus searchable articles on free and cheap things to do when the onthecheap MCP server is not available.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and agents use this skill to generate shell commands and parsing guidance for querying public On the Cheap WordPress sites when they need local event or deal data and the dedicated MCP server is unavailable.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Network requests to public On the Cheap sites could expose credentials if a user adds cookies, login tokens, or unrelated authenticated sites to the recipes.\n\nMitigation: Keep the commands credential-free and review any modified curl options or target sites before execution.\n\nRisk: Incorrect date formats, reused site-specific category ids, or truncated month grids can produce stale or incomplete event and deal results.\n\nMitigation: Resolve category ids per site, use the documented M-D-YYYY event paths, confirm page headings, and fetch day pages for complete schedules.\n\n## Reference(s):\n\n- [Ready-to-run recipes](references/recipes.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n- [Mile High on the Cheap](https://www.milehighonthecheap.com)\n- [Triangle on the Cheap](https://triangleonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Code, Guidance]\n\n**Output Format:** [Markdown with inline bash and Python code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces local command recipes for public web queries; it does not require accounts, secrets, install-time code, or persistent storage.]\n\n## Skill Version(s):\n\n1.0.0 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.4.3: 4 files, 6798 bytes\n\nFiles: references/recipes.md (6607b), skill-card.md (2690b), SKILL.md (3849b), _meta.json (127b)\n\nFile v0.4.3:SKILL.md\n\n---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```\n\n### The date format fails silently — get it right\n\nThe path is US **`M-D-YYYY`**, not ISO. An ISO date does not 404; it parses to\na nonsense timestamp and renders **January 1, 1970**, so you get confidently\nwrong data:\n\n```\n/events/view-date/7-25-2026/    ->  Saturday, July 25, 2026   ✅\n/events/view-date/2026-07-25/   ->  Thursday, January 1, 1970 ❌\n```\n\nAlways confirm the `h2.lotc-event` heading matches the date you asked for.\n\n### Month pages are truncated\n\nEach calendar cell shows at most **four** listings plus a `+N more events`\nlink. For one August the grid rendered 124 listings against a true total of\n510. Use the month view to find busy days, then fetch the day page for the\ncomplete schedule — never present the grid as the full month.\n\nSee `references/recipes.md` for ready-to-run extraction commands.\n\nFile v0.4.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"0.4.3\",\n  \"publishedAt\": 1789395293281\n}\n\nFile v0.4.3:references/recipes.md\n\n# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=dippin-dots-ice-cream-day\" \\\n| python3 -c 'import sys,json,html,re\np=json.load(sys.stdin)[0]\nprint(html.unescape(p[\"title\"][\"rendered\"])); print(p[\"link\"]); print()\nprint(re.sub(r\"\\s+\",\" \",html.unescape(re.sub(r\"<[^>]+>\",\" \",p[\"content\"][\"rendered\"]))).strip())'\n```\n\n## Events\n\n### One day, as a table\n\nConvert the ISO date to `M-D-YYYY` first — an ISO path silently returns 1970.\n\n```bash\nday() {                       # usage: day 2026-07-25\n  local iso=\"$1\"\n  local path\n  path=$(python3 -c \"import sys;y,m,d=sys.argv[1].split('-');print(f'{int(m)}-{int(d)}-{y}')\" \"$iso\")\n  curl -sLA \"$UA\" \"$BASE/events/view-date/$path/\" \\\n  | python3 -c '\nimport sys,re,html\nh=sys.stdin.read()\nhead=re.search(r\"<h2 class=\\\"lotc-event\\\">(.*?)</h2>\",h,re.S)\nprint(\"==\", html.unescape(re.sub(\"<[^>]+>\",\"\",head.group(1))).strip() if head else \"(no heading)\")\n# Split on the row marker as a REGEX, not a literal string: a featured listing\n# carries an extra class (\"lotc-v2 row event featured\"), so an exact-string\n# split silently drops it. Then read each row via its anchor and meta, since a\n# featured row also nests its text beside an image column.\nfor row in re.split(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",h)[1:]:\n    a=re.search(r\"<h3><a href=\\\"([^\\\"]+)\\\"[^>]*>(.*?)</a>\",row,re.S)\n    m=re.search(r\"<p class=\\\"meta\\\">(.*?)</p>\",row,re.S)\n    if not a: continue\n    title=html.unescape(re.sub(\"<[^>]+>\",\"\",a.group(2))).strip()\n    segs=[s.strip() for s in html.unescape(re.sub(\"<[^>]+>\",\"\",m.group(1))).split(\"|\") if s.strip()] if m else []\n    time=segs[0] if segs else \"\"\n    if len(segs)>=3: price,venue=segs[1],segs[2]\n    elif len(segs)==2: price,venue=(\"\",segs[1]) if not re.match(r\"^(FREE|\\$)\",segs[1],re.I) else (segs[1],\"\")\n    else: price=venue=\"\"\n    print(f\"{time:22} {price:14} {title[:58]:60} {venue[:34]}\")'\n}\n\nday 2026-07-25\n```\n\nTwo shapes the parsing must survive, both present on the live site:\n\n- **Two-segment meta.** A listing with no price emits `All Day | AMC Theatres,\n  participating locations`. Reading segment two positionally reports the venue\n  as the price — hence the `FREE|$` check above.\n- **Featured listings** differ twice over: they carry an extra class\n  (`lotc-v2 row event featured`) and nest their text in a `col-sm-9` column\n  beside an image column instead of a plain `col-sm-12`. Matching the row class\n  as an exact string drops them — 2026-07-25 has 38 listings but reports 37.\n  Hence the `[^\"]*` in the split pattern above.\n\n### Free listings only\n\n```bash\nday 2026-07-25 | grep -i 'FREE'\n```\n\n### Month overview with true per-day counts\n\n```bash\nmonth() {                     # usage: month 2026-08\n  local mm=${1#*-} yyyy=${1%-*}\n  curl -sLA \"$UA\" \"$BASE/events/calendar/$mm-$yyyy/\" \\\n  | python3 -c '\nimport sys,re\nh=sys.stdin.read()\ntotal=0\nfor cell in re.findall(r\"<td class=\\\"calendar-day\\\">(.*?)</td>\",h,re.S):\n    d=re.search(r\"view-date/(\\d{1,2})-(\\d{1,2})-(\\d{4})\",cell)\n    if not d: continue\n    shown=len(re.findall(r\"<div class=\\\"lotc-v2 row event[^\\\"]*\\\">\",cell))\n    more=re.search(r\"\\+\\s*(\\d+)\\s*more\",cell)\n    n=shown+(int(more.group(1)) if more else 0)\n    total+=n\n    print(f\"{d.group(3)}-{int(d.group(1)):02d}-{int(d.group(2)):02d}  {n:3d} events\" + (\"  (cell shows %d)\"%shown if more else \"\"))\nprint(f\"\\nmonth total: {total}\")'\n}\n\nmonth 2026-08\n```\n\nExpected for Charlotte in one August: 31 days, **510** events total, while the\ngrid itself rendered only 124 — the difference is the `+N more` overflow. Always report the\ncomputed total, and fetch the day page for a complete listing.\n\nFile v0.4.3:skill-card.md\n\n## Description:\n\notc-curl helps agents query the On the Cheap network from the shell for daily local event listings and searchable articles when the onthecheap MCP server is unavailable.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users and developers use this skill to have an agent retrieve public local event and deal information from On the Cheap city sites with curl and jq guidance when the MCP server is unavailable.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Source sites can change event dates, availability, or page structure after the agent retrieves data.\n\nMitigation: Verify event dates and availability against the source pages before acting on results.\n\nRisk: Expired article categories use site-specific IDs; a reused or hardcoded ID can allow retired offers into results.\n\nMitigation: Resolve the expired category ID for the selected city site before querying articles.\n\nRisk: Event day URLs silently accept the wrong date format and can return January 1, 1970 data.\n\nMitigation: Convert dates to M-D-YYYY paths and confirm the rendered event heading matches the requested date.\n\nRisk: Monthly calendar pages show truncated event lists.\n\nMitigation: Use month pages only to identify busy days, then fetch day pages for complete listings.\n\nRisk: The skill intentionally makes public web requests to On the Cheap city sites.\n\nMitigation: Install and use it only when public local event and deal lookup is intended; do not provide credentials.\n\n## Reference(s):\n\n- [Ready-to-run recipes](references/recipes.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/otc-curl)\n- [Publisher profile](https://clawhub.ai/user/chrischall)\n- [Charlotte On the Cheap](https://www.charlotteonthecheap.com)\n- [Mile High on the Cheap](https://www.milehighonthecheap.com)\n- [Triangle on the Cheap](https://triangleonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, guidance]\n\n**Output Format:** [Markdown guidance with inline bash, jq, and Python command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces command guidance for public web requests; does not require credentials or persistent storage.]\n\n## Skill Version(s):\n\n0.4.3 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.","readmeExcerpt":"Skill: otc-curl Owner: chrischall Summary: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available. Tags: latest:1.1.7 Version hi","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'"},{"language":"bash","snippet":"curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'"},{"language":"bash","snippet":"curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'"},{"language":"bash","snippet":"curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'"},{"language":"bash","snippet":"# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'"},{"language":"bash","snippet":"curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\""}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: otc-curl\ndescription: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available.\n---\n\n# On the Cheap via curl\n\nThe \"on the Cheap\" sites are public WordPress sites with open REST APIs and no\nbot wall, so they are read directly with `curl` — **no API key, no login, no\nbrowser bridge**. Nothing needs installing beyond `curl` and `jq`.\n\nIf the `onthecheap` MCP server is available, prefer its tools; they handle the\ndate-format, truncation and category-id traps below for you. This skill is the\nfallback.\n\n```bash\n# Pick a city — every site below works identically.\nBASE=https://www.charlotteonthecheap.com     # charlotte\n# BASE=https://www.milehighonthecheap.com    # denver\n# BASE=https://triangleonthecheap.com        # raleigh/durham/chapel hill\n# also: atlanta, chicago, columbus, greaterseattle, kansascity, miami,\n#       orlando, portlandliving, rva, southernmaine  (all *onthecheap.com)\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n```\n\n## Articles — JSON\n\n```bash\n# Search (slim fields; drop _fields for full records). $EXPIRED is resolved below.\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" | jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n\n# One article by slug\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?slug=<slug>\" | jq -r '.[0].content.rendered'\n\n# Filter ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n```\n\n**Always exclude the `expired` category — and look its id up first.** Retired\ndeals are parked there (a third to two-thirds of a site's posts), so without\nthe filter you show the user offers that no longer exist.\n\n**The id differs on every site** (`2`, `379`, `4483`, `6193`, `7803`, `16289`, …),\nso never hardcode one — an id from another site matches nothing and silently\ndisables the filter:\n\n```bash\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id')\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=10&categories_exclude=$EXPIRED&_fields=date,title,link\"\n```\n\nTitles and excerpts come back HTML-entity-encoded (`&#8217;`, `&#8212;`). See\n`references/recipes.md` for a decode filter.\n\n## Events — HTML\n\nEvents are **not** in the REST API (`/wp-json/wp/v2/event` → 404); they are\nparsed from rendered HTML. Every city site uses the same markup and routing\n(the national hub, livingonthecheap.com, has no local calendar).\n\n```bash\n# One day  — note the US M-D-YYYY order\ncurl -sLA \"$UA\" \"$BASE/events/view-date/7-25-2026/\"\n\n# One month\ncurl -sLA \"$UA\" \"$BASE/events/calendar/08-2026/\"\n```"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"otc-curl\",\n  \"version\": \"1.1.7\",\n  \"publishedAt\": 1791588526243\n}"},{"path":"references/recipes.md","content":"# Ready-to-run recipes\n\nAll verified live on 2026-07-19. Set once:\n\n```bash\nBASE=https://www.charlotteonthecheap.com   # or any sister site, see SKILL.md\nUA='onthecheap-skill (+https://github.com/chrischall/onthecheap-mcp)'\n\n# Resolve the \"expired\" category for THIS site — the id differs on every one\n# (2, 379, 4483, 6193, 7803, 16289, …), so a hardcoded value silently matches\n# nothing and serves dead deals as live.\nEXPIRED=$(curl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?slug=expired&_fields=id\" | jq -r '.[0].id // empty')\necho \"expired category on this site: ${EXPIRED:-none}\"\n```\n\n## Articles\n\n### Search, formatted\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+concert&per_page=10&categories_exclude=$EXPIRED&_fields=id,slug,date,link,title,excerpt\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\\n    \\(.link)\"'\n```\n\n### Decode HTML entities\n\nWordPress leaves entities in rendered fields (`Bashes &#8212; free supplies`).\n`jq` has no HTML decoder, so pipe through Python:\n\n```bash\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?per_page=5&categories_exclude=$EXPIRED&_fields=date,title\" \\\n| python3 -c 'import sys,json,html\nfor p in json.load(sys.stdin):\n    print(p[\"date\"][:10], html.unescape(p[\"title\"][\"rendered\"]))'\n```\n\n### Total match count\n\nThe count is in a header, not the body:\n\n```bash\ncurl -sIA \"$UA\" \"$BASE/wp-json/wp/v2/posts?search=free+museum&categories_exclude=$EXPIRED\" | grep -i x-wp-total\n```\n\n### Filter by category and location\n\n```bash\n# Discover ids\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/categories?per_page=100&orderby=count&order=desc&_fields=id,slug,count\" | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/locations?per_page=100&orderby=count&order=desc&_fields=id,slug,count\"  | jq -r '.[] | \"\\(.id)\\t\\(.count)\\t\\(.slug)\"'\n\n# Kids events (4) in Lake Norman (6264), published this year\ncurl -sA \"$UA\" \"$BASE/wp-json/wp/v2/posts?categories=4&locations=6264&after=2026-01-01T00:00:00&per_page=20&categories_exclude=$EXPIRED&_fields=date,title,link\" \\\n| jq -r '.[] | \"\\(.date[0:10])  \\(.title.rendered)\"'\n```\n\nThe `jq` one-liners above print titles exactly as WordPress returns them, so\nentities show through (`Turtle &amp; Tortoise`, `Birkdale Buds &#8212; free`).\nSwap in the Python decoder from \"Decode HTML entities\" before showing titles to\na user.\n\n**Term ids are per-site — always look them up rather than reusing these.** For\nCharlotte (2026-07-19) they happened to be: categories `charlotte-kids` 4,\n`charlotte-music` 35, `charlotte-food` 5, `festivals` 45, `museums` 13;\nlocations `center-city` 6276, `lake-norman` 6264, `south-end` 6260. The same\nslugs carry completely different ids on Denver or Atlanta, so the discovery\ncommands above are the reliable path.\n\n`after`/`before` compare against a full timestamp, so widen a bare date:\n`after=YYYY-MM-DDT00:00:00`, `before=YYYY-MM-DDT23:59:59`. Without the time on\n`before`, that day's own posts are dropped.\n\n### Full text of one article\n\n```bash\ncurl -"},{"path":"skill-card.md","content":"## Description:\n\nFind local events and inexpensive activities on On the Cheap city sites using public web pages and WordPress APIs when the onthecheap MCP server is unavailable.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople and agents looking for free or inexpensive local events and deals can search On the Cheap city sites without the onthecheap MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Requests may go to an unintended city site.\n\nMitigation: Check the city BASE value before running curl commands.\n\nRisk: Expired deals or incomplete or misdated event listings may mislead users.\n\nMitigation: Resolve the site's expired category, confirm day-page dates, and fetch full day pages instead of treating the month grid as complete.\n\n## Reference(s):\n\n- [otc-curl on ClawHub](https://clawhub.ai/chrischall/skills/otc-curl)\n- [On the Cheap extraction recipes](references/recipes.md)\n- [Charlotte on the Cheap](https://www.charlotteonthecheap.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with bash examples and event or deal summaries]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [City-specific public listings and article links; availability and prices should be checked at the source.]\n\n## Skill Version(s):\n\n1.1.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available. Skill: otc-curl Owner: chrischall Summary: Query the On the Cheap network (charlotteonthecheap.com, milehighonthecheap.com and ~12 sister city sites) from the shell with curl and jq — daily local event listings with times/prices/venues, plus searchable articles on free and cheap things to do. Use when the user wants local event or deal data and the onthecheap MCP server is not available. Tags: latest:1.1.7 Version hi","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1254,"uniquenessScore":48,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T00:17:50.983Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T00:17:50.983Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T03:55:36.312Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}