{"id":"5dafd201-0bf4-4fc8-b9b2-ee6a506cb299","entityType":"agent","slug":"clawhub-chrischall-crowntowncompost-mcp","name":"crowntowncompost-mcp","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-crowntowncompost-mcp","canonicalPath":"/agent/clawhub-chrischall-crowntowncompost-mcp","generatedAt":"2026-10-10T15:52:51.790Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T13:22:17.589Z","emptyReason":null},"description":"Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered pages. Use when you want Crown Town Compost data without the MCP, in a script, or on a machine where the MCP isn't installed. Skill: crowntowncompost-mcp Owner: chrischall Summary: Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.4K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:crowntowncompost-mcp","sourceUrl":"https://clawhub.ai/chrischall/crowntowncompost-mcp","homepage":"https://clawhub.ai/chrischall/skills/crowntowncompost-mcp","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/crowntowncompost-mcp","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/crowntowncompost-mcp","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":63,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — "},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T13:22:17.589Z","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-10T13:22:17.589Z","emptyReason":null},"stars":null,"forks":null,"downloads":1414,"packageName":null,"latestVersion":"1.1.5","tractionLabel":"1.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T13:22:17.589Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T13:22:17.589Z","lastCrawledAt":"2026-10-10T13:22:17.589Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T13:22:17.589Z","lastVerifiedAt":null,"highlights":[{"version":"1.1.5","createdAt":"2026-10-09T23:26:10.444Z","changelog":"- Removed the file skill-card.md from the repository. - No changes to functionality or documentation in this release.","fileCount":4,"zipByteSize":8461},{"version":"1.1.4","createdAt":"2026-10-07T13:34:34.330Z","changelog":"- Removed the skill-card.md file from the repository. - No functional or user-facing changes; this is a documentation cleanup release.","fileCount":4,"zipByteSize":8549},{"version":"1.1.3","createdAt":"2026-10-05T02:48:11.052Z","changelog":"- Removed the skill-card.md file. - No changes to core logic or user-facing functionality.","fileCount":4,"zipByteSize":8473},{"version":"1.1.2","createdAt":"2026-10-03T01:41:27.452Z","changelog":"- Removed the file skill-card.md from the project. - No changes were made to the SKILL.md content or functionality.","fileCount":4,"zipByteSize":8523},{"version":"1.1.1","createdAt":"2026-09-25T15:56:56.675Z","changelog":"- Removed the file skill-card.md from the project. - No changes to code or functionality. - Documentation and usage instructions remain unchanged.","fileCount":4,"zipByteSize":8535},{"version":"1.1.0","createdAt":"2026-09-24T15:11:24.916Z","changelog":"- Removed the file: skill-card.md - No changes were made to the code or documentation beyond file removal.","fileCount":4,"zipByteSize":8532},{"version":"1.0.3","createdAt":"2026-09-23T21:39:44.272Z","changelog":"- Removed the outdated skill card file (skill-card.md). - Updated references to endpoint details; see the endpoints.md file for full field lists. - No changes to usage or API methods; documentation clarification only.","fileCount":4,"zipByteSize":8643},{"version":"1.0.2","createdAt":"2026-09-23T15:44:33.954Z","changelog":"- Removed the file: skill-card.md - No functional or user-facing changes introduced in this release.","fileCount":4,"zipByteSize":8433}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:crowntowncompost-mcp","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-crowntowncompost-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/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-10T15:52:51.785Z"}},"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-crowntowncompost-mcp/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-crowntowncompost-mcp/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-10T13:22:17.589Z","emptyReason":null},"readme":"Skill: crowntowncompost-mcp\n\nOwner: chrischall\n\nSummary: Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered pages. Use when you want Crown Town Compost data without the MCP, in a script, or on a machine where the MCP isn't installed.\n\nTags: latest:1.1.5\n\nVersion history:\n\nv1.1.5 | 2026-10-09T23:26:10.444Z | auto\n\n- Removed the file skill-card.md from the repository.\n- No changes to functionality or documentation in this release.\n\nv1.1.4 | 2026-10-07T13:34:34.330Z | auto\n\n- Removed the skill-card.md file from the repository.\n- No functional or user-facing changes; this is a documentation cleanup release.\n\nv1.1.3 | 2026-10-05T02:48:11.052Z | auto\n\n- Removed the skill-card.md file.\n- No changes to core logic or user-facing functionality.\n\nv1.1.2 | 2026-10-03T01:41:27.452Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes were made to the SKILL.md content or functionality.\n\nv1.1.1 | 2026-09-25T15:56:56.675Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes to code or functionality.\n- Documentation and usage instructions remain unchanged.\n\nv1.1.0 | 2026-09-24T15:11:24.916Z | auto\n\n- Removed the file: skill-card.md\n- No changes were made to the code or documentation beyond file removal.\n\nv1.0.3 | 2026-09-23T21:39:44.272Z | auto\n\n- Removed the outdated skill card file (skill-card.md).\n- Updated references to endpoint details; see the endpoints.md file for full field lists.\n- No changes to usage or API methods; documentation clarification only.\n\nv1.0.2 | 2026-09-23T15:44:33.954Z | auto\n\n- Removed the file: skill-card.md\n- No functional or user-facing changes introduced in this release.\n\nv1.0.1 | 2026-09-21T04:13:44.120Z | auto\n\n- Removed the file skill-card.md.\n- No other changes to functionality or documentation.\n\nv1.0.0 | 2026-09-20T02:49:20.630Z | auto\n\n- Removed the file skill-card.md.\n- No changes to code or functionality.\n- Documentation and core skill features unchanged.\n\nv0.6.0 | 2026-09-17T23:38:09.054Z | auto\n\n- Removed the skill-card.md file.\n- No changes to functionality or documentation aside from file removal.\n\nv0.5.2 | 2026-09-14T14:11:27.325Z | auto\n\n- Removed the file: skill-card.md\n- No changes to code or functionality; this update is file cleanup only.\n\nv0.5.1 | 2026-09-10T17:49:45.481Z | auto\n\n- Removed the unused file: skill-card.md.\n- No other changes to functionality or documentation.\n\nv0.5.0 | 2026-09-04T22:20:02.038Z | auto\n\n- Removed the file: skill-card.md.\n- No functional or code changes; documentation or metadata cleanup only.\n\nv0.4.0 | 2026-08-28T11:34:40.403Z | auto\n\n- Removed the file skill-card.md.\n- No new features or functionality added.\n- No user-facing changes in commands, endpoints, or documentation.\n\nv0.3.1 | 2026-08-09T21:02:22.267Z | auto\n\n- Removed the file: skill-card.md\n- No other user-facing changes in this release.\n\nv0.3.0 | 2026-07-31T15:53:48.193Z | auto\n\n- Removed the obsolete skill-card.md file.\n- No changes to functionality or documentation.\n- Cleanup release; skill usage remains the same.\n\nv0.2.0 | 2026-07-31T15:06:49.699Z | auto\n\n- Removed the file: skill-card.md\n- No other changes to features, functionality, or documentation\n\nv0.1.0 | 2026-07-31T14:42:24.500Z | auto\n\n- Initial release of crowntown-portal: access the Crown Town Compost customer portal using curl, without running the MCP server.\n- Enables command-line and scriptable access to service history, billing, and account data via authenticated requests.\n- Detailed setup and authentication instructions, including handling Django CSRF requirements.\n- Provides ready-to-use examples for retrieving pickups, invoices, impact stats, service calendar, and making account changes.\n- Designed for customers who need data access without installing the MCP or in automation workflows.\n\nArchive index:\n\nArchive v1.1.5: 4 files, 8461 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (1883b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.1.5:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1791588370444\n}\n\nFile v1.1.5:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.1.5:skill-card.md\n\n## Description:\n\nGuides agents in accessing a Crown Town Compost customer account through shell requests to review service history, invoices, upcoming pickups, and account details without the MCP server.\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\nCustomers and their authorized agents use shell-based guidance to check pickup history, billing, and upcoming service, or to manage their Crown Town Compost account.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Persistent session cookies could expose a live customer account if the cookie jar is accessed by others.\n\nMitigation: Use a temporary cookie jar restricted to the account owner (chmod 600), and delete it after use.\n\nRisk: Commands can skip service, change profile details, contact support, report missed pickups, or request cancellation.\n\nMitigation: Require explicit confirmation before any account-changing action and re-read the account afterward to verify the result.\n\n## Reference(s):\n\n- [Crown Town Compost portal endpoint reference](artifact/references/endpoints.md)\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands can retrieve account data or submit account changes when authorized.]\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, 8549 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (2182b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.1.4:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.4\",\n  \"publishedAt\": 1791380074330\n}\n\nFile v1.1.4:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.1.4:skill-card.md\n\n## Description:\n\nGuides agents in accessing Crown Town Compost customer portal information and account actions through authenticated shell requests without running an MCP server.\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\nCrown Town Compost customers and their agents use this skill to review pickups, invoices, and upcoming service, or to prepare authorized account and service requests without installing the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal credentials and session cookies can be exposed through shell environment or cookie files.\n\nMitigation: Use a temporary cookie jar with restrictive permissions, avoid leaving passwords exported, and clear session material after use.\n\nRisk: Portal requests can change service or account details or send messages.\n\nMitigation: Require human confirmation before skips, account updates, missed-pickup reports, support messages, or cancellation requests; reread the account to verify any change.\n\nRisk: Billing and account responses can contain sensitive customer information.\n\nMitigation: Avoid logging raw billing or account data and share only the minimum details needed.\n\n## Reference(s):\n\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n- [Crown Town Compost customer portal](https://portal.crowntowncompost.com)\n- [Portal endpoint reference](references/endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, shell commands, guidance]\n\n**Output Format:** [Markdown with shell commands and portal data summaries]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Authenticated responses may include personal account and billing information.]\n\n## Skill Version(s):\n\n1.1.4 (source: server-resolved 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, 8473 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (1935b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1791168491052\n}\n\nFile v1.1.3:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.1.3:skill-card.md\n\n## Description:\n\nGuides an agent through accessing Crown Town Compost customer information and submitting service requests from a shell without an MCP server.\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\nCrown Town Compost customers and their authorized agents can check pickup history, invoices, upcoming service, and account details, or prepare service and account changes.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Authenticated requests can change live service or account settings.\n\nMitigation: Require explicit review before skip, account update, support, missed-pickup, or cancellation-related requests; re-read the account to verify changes.\n\nRisk: Persistent login cookies may expose access to the customer account.\n\nMitigation: Keep the cookie jar temporary or restrict it to the owner with chmod 600, and delete it after use.\n\nRisk: Account responses can contain contact and billing information.\n\nMitigation: Avoid logging or exporting billing and contact data unless needed.\n\n## Reference(s):\n\n- [Portal endpoint reference](artifact/references/endpoints.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May surface private account and billing information; account-changing requests require review.]\n\n## Skill Version(s):\n\n1.1.3 (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.2: 4 files, 8523 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (2069b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1790991687452\n}\n\nFile v1.1.2:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.1.2:skill-card.md\n\n## Description:\n\nGuides agents through accessing a Crown Town Compost customer account from a shell to review service history, invoices, upcoming pickups, and account details without running an MCP server.\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\nCrown Town Compost customers and developers can use this skill to check pickups, service dates, and invoices through their customer portal, or to prepare account updates and service requests with confirmation.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal credentials and saved session cookies can expose a customer's account.\n\nMitigation: Use a password manager or interactive prompt instead of long-lived exported passwords; restrict access to and delete the cookie jar after use.\n\nRisk: Skip requests, account updates, support messages, missed-pickup reports, and cancellation requests can change an account or contact the service provider.\n\nMitigation: Require deliberate confirmation before submitting these requests and recheck the account afterward to verify the intended change.\n\n## Reference(s):\n\n- [Crown Town Compost portal endpoint reference](references/endpoints.md)\n- [Crown Town Compost customer portal](https://portal.crowntowncompost.com)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands may return portal account data, including service and billing history.]\n\n## Skill Version(s):\n\n1.1.2 (source: server-resolved 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.1: 4 files, 8535 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (2161b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.1.1:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.1\",\n  \"publishedAt\": 1790351816675\n}\n\nFile v1.1.1:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.1.1:skill-card.md\n\n## Description:\n\nGuides agents in accessing Crown Town Compost pickup history, invoices, service dates, and account features through the customer portal using shell commands instead of an MCP server.\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\nCrown Town Compost customers and their authorized agents can retrieve pickup and billing information or manage service requests from a shell without installing the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal actions can change service and account settings despite the skill's data-access framing.\n\nMitigation: Review the skill before installing and require explicit human approval before skips, account updates, support messages, missed-pickup reports, or cancellation requests.\n\nRisk: Credentials, session cookies, and exported invoices can expose private account or billing information.\n\nMitigation: Protect credentials and the cookie jar; delete the cookie jar and billing exports when finished.\n\nRisk: Partial account updates can turn off notification preferences, and a redirect does not prove a change persisted.\n\nMitigation: Read existing settings before updates, preserve all fields, and re-read the portal after any change.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n- [Crown Town Compost portal endpoint reference](references/endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and portal-response guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May guide retrieval of account data and consequential service or account changes.]\n\n## Skill Version(s):\n\n1.1.1 (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.0: 4 files, 8532 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (2051b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.1.0:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.1.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.0\",\n  \"publishedAt\": 1790262684916\n}\n\nFile v1.1.0:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.1.0:skill-card.md\n\n## Description:\n\nAccesses the Crown Town Compost customer portal from shell-based workflows to retrieve service, billing, account, and scheduling information and prepare portal requests with curl.\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 operators with Crown Town Compost portal accounts use this skill to script authenticated portal access for pickup history, invoices, upcoming service days, account details, and carefully reviewed account actions.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can enable real account changes and persistent login cookies.\n\nMitigation: Require explicit approval before service skips, profile changes, support messages, cancellation steps, or other write actions; re-read portal state after any accepted write.\n\nRisk: Cookie jars and exported billing CSVs may expose account or billing data.\n\nMitigation: Store cookie jars with restrictive permissions or in temporary files, delete them after use, and treat exported billing CSVs as sensitive.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n- [Crown Town Compost portal](https://portal.crowntowncompost.com)\n- [Endpoint reference](artifact/references/endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with inline shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include curl commands and parsing guidance for authenticated portal requests.]\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.3: 4 files, 8643 bytes\n\nFiles: references/endpoints.md (7602b), skill-card.md (2357b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1790199584272\n}\n\nFile v1.0.3:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n`date` is bound to a datepicker with format `yyyy-mm-dd` — send ISO. Success is a\n**302**; a **200** is Django re-rendering the form with errors (look for\n`ul.errorlist`), meaning nothing was submitted.\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=2026-07-24' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit `on`.\n\n```sh\nctpost /accounts/update/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'first_name=Test' --data-urlencode 'last_name=User' \\\n  --data-urlencode 'phone=555-555-5555' \\\n  --data-urlencode 'service_notifications=on'      # send_email_reminders omitted ⇒ off\n\n# verify\nctget /accounts/update/ | grep -oE 'name=\"phone\" value=\"[^\"]*\"'\n```\n\n### Contact support — `/accounts/support/`\n\nThe form pre-fills `email` and `phone` from the account; send them (a browser\ndoes). As above, a 302 means accepted and a 200 re-render means rejected.\n\n```sh\nctpost /accounts/support/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'message=Please confirm my next pickup date.' \\\n  --data-urlencode 'email=you@example.com'\n```\n\n### Request cancellation — `/accounts/cancellation-request/`\n\nField names are **per-location dynamic** (`cancel_location_<locId>`,\n`final_route_<locId>`, `cf_<n>`), so read the GET form first and echo its fields\nback:\n\n```sh\nctget /accounts/cancellation-request/ | grep -oE '<(input|select)[^>]*name=\"[^\"]*\"'\n```\n\nNot scripted here on purpose — cancelling service is a consequential write; do\nit deliberately.\n\n---\n\n## Out of scope\n\n`/accounts/shop/`, `/accounts/checkout/`, `/gift-certificates/` involve payment.\nOpen them in a browser; don't script card entry.\n\nFile v1.0.3:skill-card.md\n\n## Description:\n\nAccess the Crown Town Compost customer portal from a shell with curl to retrieve pickup history, invoices, upcoming service days, skips, and account details without running the crowntowncompost-mcp server.\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 with an existing Crown Town Compost portal account use this skill to retrieve service, billing, and account information from a shell. The skill also documents account-changing POST workflows that should be used only with deliberate user approval.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill includes live account-changing workflows for service, profile, support, and cancellation state.\n\nMitigation: Require explicit user approval before any POST that changes account state, and re-read the portal after each write to verify the result.\n\nRisk: The cookie jar and billing exports may expose account access or sensitive billing information if left unsecured.\n\nMitigation: Protect or delete the cookie jar after use, and store downloaded billing files only in a secure location.\n\nRisk: Portal credentials are needed to create a session.\n\nMitigation: Read passwords from stdin or a credential manager rather than placing secrets directly in shell command arguments.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n- [Crown Town Compost endpoint reference](references/endpoints.md)\n- [Crown Town Compost portal](https://portal.crowntowncompost.com)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown with inline shell commands and endpoint examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Includes curl request patterns, CSRF handling notes, jq examples, and verification steps for account writes.]\n\n## Skill Version(s):\n\n1.0.3 (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 v1.0.2: 4 files, 8433 bytes\n\nFiles: references/endpoints.md (7245b), skill-card.md (2297b), SKILL.md (8215b), _meta.json (139b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'\n```\n\n**Success** = `HTTP/2 302` + a `location:` that is NOT `/accounts/login/`, and\na `sessionid` now in the jar. A `200` means the login page re-rendered — wrong\nusername/password. Confirm the jar:\n\n```sh\ngrep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\"\n```\n\nThe `csrftoken` cookie **rotates on login** — always re-read it from the jar\nbefore each POST:\n\n```sh\ncsrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }\n```\n\n## The two JSON endpoints (the good stuff)\n\nBoth are **POST**, form-urlencoded, and return `{meta, qs, data[]}`. They use\nthe **Metronic KTDatatable** grammar — `pagination[page]`, `pagination[perpage]`,\n`sort[field]`, `sort[sort]`, `query[<field>]` — *not* DataTables' `draw/start/length`.\n\n```sh\nctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}\n```\n\n### Service history (pickups)\n\n```sh\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'pagination[page]=1' \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '{total: .meta.total, pages: .meta.pages,\n       stops: [.data[] | {date, status, time: .timestamp, weight, services}]}'\n```\n\nFilter by outcome — **the values are lowercase**; a capitalized `Missing`\nsilently returns zero rows:\n\n```sh\nctpost /accounts/stops/api/ --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=50' | jq '.meta.total, [.data[].date]'\n```\n\nValid: `success | missing | empty | inaccessible | unacceptable` (omit for all).\n\n### Billing history (invoices)\n\n```sh\nctpost /accounts/billing-history/api/ \\\n  --data-urlencode 'pagination[perpage]=20' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq '[.data[] | {number, date, amount, status, is_payable, pay: .hosted_invoice_url}]'\n```\n\nOpen invoices are `is_payable: true`; `hosted_invoice_url` / `invoice_pdf` /\n`receipt_url` are Stripe links. **Pay in a browser** — don't script payments.\n\n## Server-rendered pages (GET, parse the HTML)\n\n| What | Path |\n|---|---|\n| Dashboard (subscription, next service, addresses) | `/accounts/` |\n| Environmental impact numbers (htmx fragment) | `/accounts/impact-statistics/` |\n| Account details form (name, phone, notification prefs) | `/accounts/update/` |\n| Service calendar (upcoming + skip buttons) | `/accounts/service-calendar/` |\n| Service history page (shell around the JSON above) | `/accounts/service-history/` |\n| Support form | `/accounts/support/` |\n\n```sh\nctget() { curl -s -b \"$JAR\" -c \"$JAR\" \"$CT$1\"; }\n```\n\nImpact numbers are the easiest scrape (plain text in a small fragment):\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\nUpcoming skippable service days — each `.submit-skip` button carries the ids a\nskip needs:\n\n```sh\nctget /accounts/service-calendar/ \\\n  | grep -o '<button[^>]*submit-skip[^>]*>' \\\n  | sed -E 's/.*data-action=\"([^\"]*)\".*data-clid=\"([^\"]*)\".*data-rid=\"([^\"]*)\".*data-route-date=\"([^\"]*)\".*/\\4  rid=\\3 clid=\\2 action=\\1/'\n```\n\n`action=\"skip\"` = currently scheduled (skipping it will skip it);\n`action=\"unskip\"` = already skipped.\n\n## Writes (all POST; see `references/endpoints.md` for full field lists)\n\n**A 302 is not proof a write persisted — always re-read to verify.**\n\n```sh\n# Skip an upcoming service (rid/clid/action from the calendar above)\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip' >/dev/null\n# verify: the same day's button should now read action=\"unskip\"\n```\n\nOther write endpoints (each POSTs to its own URL, with `csrfmiddlewaretoken`):\n`/accounts/report-missed-pickup/` (`date`, `comment`),\n`/accounts/update/` (`first_name`, `last_name`, `phone`, `send_email_reminders`,\n`service_notifications` — **read-modify-write**: re-send every field, since\nomitted checkboxes mean \"off\"), `/accounts/support/` (`message`, `email`, `phone`),\n`/accounts/cancellation-request/` (per-location dynamic field names — read the\nGET form first).\n\n## Session expiry\n\nAn authed request that returns the login page (or redirects to\n`/accounts/login/`) means the session expired — re-run the login block. Detect it:\n\n```sh\nctget /accounts/ | grep -q 'm_login_signin_submit' && echo \"session expired\"\n```\n\n## Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| `403` on any POST | Missing `X-CSRFToken` / `Origin` / `Referer`, or a stale `csrftoken` (re-read it from the jar after login) |\n| Login returns `200` | Wrong username/password (the page just re-rendered) |\n| JSON endpoint returns HTML | Session expired — log in again |\n| `query[status]` returns 0 rows | You capitalized the value; use lowercase |\n| Empty `data[]` with a large `perpage` | You're past the last page — check `meta.pages` |\n\nFile v1.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.0.2\",\n  \"publishedAt\": 1790178273954\n}\n\nFile v1.0.2:references/endpoints.md\n\n# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'\n\n# Total billed over the returned window\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].amount | gsub(\"[$,]\";\"\") | tonumber] | add'\n```\n\nCSV export (returns a file):\n\n```sh\nctpost /accounts/billing-history/csv/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'qs=' -o billing.csv\n```\n\n---\n\n## GET pages worth scraping\n\n### `/accounts/` — dashboard\n\nServer-rendered. Useful anchors:\n\n- `.account-status-item` × 3 → `Account Status`, `Active Subscriptions`, `Next Service`\n- `.m-badge--wide` → subscription status (`Active`)\n- `$44.00/month` → price + period\n- `Renews …` → **rendered twice** (a short `August 1` then the full\n  `August 1, 2026, 1:00 a.m.`). Match the form that carries a year, or you get the short one.\n- The Service Address card's `<table>` → address + service day(s)\n\n```sh\nctget /accounts/ | grep -o 'account-status-item[^<]*<[^>]*>[^<]*' | head\nctget /accounts/ | grep -oE '\\$[0-9,]+\\.[0-9]{2}/[a-z]+'\n```\n\n### `/accounts/impact-statistics/` — htmx fragment\n\n```sh\nctget /accounts/impact-statistics/ \\\n  | grep -oE '[0-9.,]+ (lbs diverted|seedlings planted|miles offset|gallons of gas)'\n```\n\n### `/accounts/update/` — account details form\n\nCurrent values live in the `value=\"\"` attributes; the two preference toggles are\ncheckboxes (`checked` present = on).\n\n```sh\nctget /accounts/update/ \\\n  | grep -oE 'name=\"(first_name|last_name|phone)\" value=\"[^\"]*\"'\nctget /accounts/update/ | grep -oE '<input[^>]*name=\"(send_email_reminders|service_notifications)\"[^>]*>'\n```\n\n### `/accounts/service-calendar/` — upcoming days + skip ids\n\nEach skippable day renders:\n\n```html\n<button class=\"btn m-btn btn-sm submit-skip\" data-action=\"skip\" data-clid=\"3360\"\n        data-rid=\"2815\" data-route-date=\"Aug. 7, 2026\" data-skip-has-credit=\"false\">Skip Service</button>\n```\n\nNon-skippable days render a plain `Not Skippable` label instead.\n\n---\n\n## Writes\n\nEvery POST carries `csrfmiddlewaretoken` plus the headers from `SKILL.md`.\n**Re-read after every write — a 302 only means the request was accepted.**\n\n### Skip / un-skip a service — `/accounts/service-calendar/skip-service/`\n\n```sh\nctpost /accounts/service-calendar/skip-service/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'rid=2815' --data-urlencode 'clid=3360' \\\n  --data-urlencode 'action=skip'\n\n# verify — the button for that rid/clid should now say action=\"unskip\"\nctget /accounts/service-calendar/ | grep -o 'data-action=\"[^\"]*\" data-clid=\"3360\" data-rid=\"2815\"'\n```\n\n### Report a missed pickup — `/accounts/report-missed-pickup/`\n\n```sh\nctpost /accounts/report-missed-pickup/ \\\n  --data-urlencode \"csrfmiddlewaretoken=$(csrf)\" \\\n  --data-urlencode 'date=Jul 24, 2026' \\\n  --data-urlencode 'comment=Bin was out by 6am, not collected.'\n```\n\n### Update account details — `/accounts/update/` (read-modify-write)\n\nSend **every** field: omitted checkboxes are read as \"off\", so a partial POST\nsilently turns preferences off. Checked boxes submit\n\nArchive v1.0.1: 4 files, 8399 bytes\n\nFiles: references/endpoints.md (7245b), skill-card.md (2246b), SKILL.md (8215b), _meta.json (139b)\n\nArchive v1.0.0: 4 files, 8413 bytes\n\nFiles: references/endpoints.md (7245b), skill-card.md (2160b), SKILL.md (8215b), _meta.json (139b)","readmeExcerpt":"Skill: crowntowncompost-mcp Owner: chrischall Summary: Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"export CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\""},{"language":"sh","snippet":"# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencode \"password@-\" \\\n  --data-urlencode \"next=/accounts/\" \\\n  -o /dev/null -D - \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -iE '^(HTTP/|location:)'"},{"language":"sh","snippet":"grep -q sessionid \"$JAR\" && echo \"signed in\" || echo \"NOT signed in\""},{"language":"sh","snippet":"csrf() { awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1; }"},{"language":"sh","snippet":"curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\"},{"language":"sh","snippet":"ctpost() {  # ctpost <path> [extra --data-urlencode args...]\n  local path=\"$1\"; shift\n  curl -s -b \"$JAR\" -c \"$JAR\" \\\n    -H \"X-CSRFToken: $(csrf)\" -H 'X-Requested-With: XMLHttpRequest' \\\n    -H \"Origin: $CT\" -H \"Referer: $CT$path\" \\\n    \"$@\" \"$CT$path\"\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: crowntown-portal\ndescription: >-\n  Access the Crown Town Compost customer portal (portal.crowntowncompost.com) —\n  pickup/service history, invoices, upcoming service days, skips, account\n  details — from a shell with curl instead of running the crowntowncompost-mcp\n  server. Logs in with a Django username/password form POST to get a session\n  cookie, then curls two JSON endpoints and a few server-rendered pages. Use\n  when you want Crown Town Compost data without the MCP, in a script, or on a\n  machine where the MCP isn't installed.\n---\n\n# Crown Town Compost portal via curl (no MCP)\n\n`crowntowncompost.com` is only a marketing site. All customer data lives at\n**`portal.crowntowncompost.com`** — a **Django** app (a white-labeled\n**StopSuite** hauler platform; it CNAMEs to `crowntown.stopsuite.com`).\n\nIt is reachable **server-side**: no bot wall, no browser bridge, no `fpx`, no\nTransporter extension. Auth is a real **username + password form POST** that\nreturns an HttpOnly `sessionid` cookie. `curl` with a cookie jar reaches\neverything a signed-in customer can see.\n\nTwo of the richest surfaces are clean **JSON** endpoints (service history and\nbilling); the rest are server-rendered HTML.\n\n## The one thing that trips people up: Django CSRF\n\nEvery **POST** (including the login and the two JSON reads) needs all of:\n\n- the `csrftoken` cookie (set by any GET), sent back in the cookie jar\n- an `X-CSRFToken:` header **equal to that cookie's value**\n- `Origin:` and `Referer:` headers on the portal's own origin — Django rejects\n  HTTPS POSTs that lack them, with a 403, *before* looking at your credentials\n\nGETs need none of this. A 403 on a POST almost always means a missing header,\nnot a bad password.\n\n## One-time setup\n\n```sh\nexport CROWNTOWN_USERNAME='you@example.com'\nexport CROWNTOWN_PASSWORD='your-portal-password'   # or: op read \"op://Private/CrownTown/password\"\nexport CT=https://portal.crowntowncompost.com\nJAR=~/.cache/crowntown-cookies.txt\nmkdir -p ~/.cache && : > \"$JAR\"\n```\n\n## Log in (get the session cookie)\n\nRead the password from stdin (`password@-`) rather than putting it in argv —\n`--data-urlencode name=value` exposes the value to any local user via `ps`\nwhile the process runs.\n\n```sh\n# 1. GET the login page to pick up the csrftoken cookie + the hidden form token.\nCSRF_FORM=$(curl -s -c \"$JAR\" -b \"$JAR\" \"$CT/accounts/login/?next=/accounts/\" \\\n  | grep -o 'name=\"csrfmiddlewaretoken\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"$//')\nCSRF_COOKIE=$(awk '$6==\"csrftoken\"{print $7}' \"$JAR\" | tail -1)\n\n# 2. POST the credentials. Do NOT follow redirects (-L) here: the 302's\n#    Set-Cookie carries the sessionid, and success is \"302 away from login\".\nprintf '%s' \"$CROWNTOWN_PASSWORD\" | curl -s -c \"$JAR\" -b \"$JAR\" \\\n  -H \"X-CSRFToken: $CSRF_COOKIE\" -H \"Origin: $CT\" \\\n  -H \"Referer: $CT/accounts/login/?next=/accounts/\" \\\n  --data-urlencode \"csrfmiddlewaretoken=$CSRF_FORM\" \\\n  --data-urlencode \"username=$CROWNTOWN_USERNAME\" \\\n  --data-urlencod"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"crowntowncompost-mcp\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1791588370444\n}"},{"path":"references/endpoints.md","content":"# Crown Town Compost portal — endpoint reference\n\nReady-to-run request bodies for `portal.crowntowncompost.com` (Django /\nStopSuite). Assumes the `$CT`, `$JAR`, `csrf()`, `ctget()`, `ctpost()` helpers\nfrom `SKILL.md` are already defined and you are signed in.\n\nAll shapes were live-verified 2026-07-31 against a signed-in customer account.\n\n---\n\n## Response envelope (both JSON endpoints)\n\n```json\n{\n  \"meta\": { \"page\": 1, \"pages\": 4, \"perpage\": 20, \"total\": 61,\n            \"sort\": \"desc\", \"field\": \"date\", \"rowIds\": [157404] },\n  \"qs\": \"<serialized query state>\",\n  \"data\": [ /* rows */ ]\n}\n```\n\nRequest grammar (Metronic KTDatatable):\n\n| Param | Meaning |\n|---|---|\n| `pagination[page]` | 1-based page number |\n| `pagination[perpage]` | rows per page (20 default) |\n| `sort[field]` | column field name |\n| `sort[sort]` | `asc` \\| `desc` |\n| `query[<field>]` | per-column filter |\n| `query[generalSearch]` | free-text search |\n\n---\n\n## POST `/accounts/stops/api/` — service history\n\nSortable/filterable fields: `RecordID, address, date, status, timestamp, services`.\n\nRow:\n\n```json\n{ \"RecordID\": 157404, \"status\": \"Success\", \"timestamp\": \"9:34 a.m.\",\n  \"address\": \"123 Example Street\", \"weight\": \"35\", \"nickname\": \"\",\n  \"date\": \"Friday, Jul 31, 2026\", \"services\": \"35 x1\" }\n```\n\n- `status` displays capitalized (`Success`/`Missing`/`Empty`/`Inaccessible`/`Unacceptable`)\n  but **`query[status]` matches lowercase only**.\n- `services` is `<service code> x<count>` — `x0` on a missed stop.\n- `weight` is in lbs and is empty when nothing was collected.\n\nRecipes:\n\n```sh\n# Every missed pickup, newest first\nctpost /accounts/stops/api/ \\\n  --data-urlencode 'query[status]=missing' \\\n  --data-urlencode 'pagination[perpage]=100' \\\n  --data-urlencode 'sort[field]=date' --data-urlencode 'sort[sort]=desc' \\\n| jq -r '.data[] | \"\\(.date)  \\(.status)\"'\n\n# Total weight diverted across all recorded stops\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].weight | select(. != \"\") | tonumber] | add'\n\n# Success rate\nctpost /accounts/stops/api/ --data-urlencode 'pagination[perpage]=100' \\\n| jq '[.data[].status] | {total: length, success: map(select(.==\"Success\")) | length}'\n\n# Page through everything\nfor p in $(seq 1 4); do\n  ctpost /accounts/stops/api/ --data-urlencode \"pagination[page]=$p\" \\\n    --data-urlencode 'pagination[perpage]=20' | jq -c '.data[] | {date,status}'\ndone\n```\n\n---\n\n## POST `/accounts/billing-history/api/` — invoices\n\nRow:\n\n```json\n{ \"RecordID\": 1, \"number\": \"INV-1\", \"date\": \"Jul 1, 2026\", \"amount\": \"$44.00\",\n  \"status\": \"paid\", \"invoice_pdf\": \"https://…\", \"receipt_url\": \"https://…\",\n  \"hosted_invoice_url\": \"https://…\", \"is_payable\": false, \"invoice_id\": 11 }\n```\n\nStripe-backed. `is_payable: true` marks an open invoice.\n\n```sh\n# Open invoices with their payment links\nctpost /accounts/billing-history/api/ --data-urlencode 'pagination[perpage]=50' \\\n| jq -r '.data[] | select(.is_payable) | \"\\(.date)  \\(.amount)  \\(.hosted_invoice_url)\"'"},{"path":"skill-card.md","content":"## Description:\n\nGuides agents in accessing a Crown Town Compost customer account through shell requests to review service history, invoices, upcoming pickups, and account details without the MCP server.\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\nCustomers and their authorized agents use shell-based guidance to check pickup history, billing, and upcoming service, or to manage their Crown Town Compost account.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Persistent session cookies could expose a live customer account if the cookie jar is accessed by others.\n\nMitigation: Use a temporary cookie jar restricted to the account owner (chmod 600), and delete it after use.\n\nRisk: Commands can skip service, change profile details, contact support, report missed pickups, or request cancellation.\n\nMitigation: Require explicit confirmation before any account-changing action and re-read the account afterward to verify the result.\n\n## Reference(s):\n\n- [Crown Town Compost portal endpoint reference](artifact/references/endpoints.md)\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/crowntowncompost-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands can retrieve account data or submit account changes when authorized.]\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."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered pages. Use when you want Crown Town Compost data without the MCP, in a script, or on a machine where the MCP isn't installed. Skill: crowntowncompost-mcp Owner: chrischall Summary: Access the Crown Town Compost customer portal (portal.crowntowncompost.com) — pickup/service history, invoices, upcoming service days, skips, account details — from a shell with curl instead of running the crowntowncompost-mcp server. Logs in with a Django username/password form POST to get a session cookie, then curls two JSON endpoints and a few server-rendered","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1327,"uniquenessScore":46,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T13:22:17.589Z","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-10T13:22:17.589Z","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-10T15:52:51.790Z","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"}]}}}