{"data":[{"id":"social","name":"Social","tagline":"Post to X, Bluesky, and Threads through one headless API (BYOK).","description":"Connect your own X, Bluesky, and Threads accounts once, then post or cross-post through one API. Credentials are encrypted before we store them; per-target results and an overall status come back on every post.","pitch":"Your users connect their own accounts via OAuth. Cross-post across X, Bluesky, and Threads in one call, with per-target results. We do roughly what Buffer does, for less, because we haven't raised $46 million.","basePath":"/v1/social","category":"publish","highlights":[{"label":"Platforms","items":["X","Bluesky","Threads"]}],"status":"available","alwaysOn":true,"howItWorks":"You register a per-platform connection (BYOK credentials or an OAuth grant). For X, you bring your X app's client id/secret and complete an OAuth 2.0 PKCE flow; for Bluesky, an app password; for Threads, a long-lived Meta access token. cheapass stores these AES-256-GCM encrypted at rest. When you POST /v1/social/posts with a `targets` array (or omit it to use each platform's default connection), the API fans the text out to each target, returns per-target success/failure, and an overall status of 201 (all posted), 207 (partial), or 502 (all failed). POST /v1/social/posts/x posts to X only. GET /v1/social/platforms lists supported platforms and their capabilities.","getStarted":["cheapass login — authenticate the CLI.","cheapass keys create — mint an API key (saved to ~/.cheapass/config.json).","curl -X POST $API/v1/social/platforms/x/connections -H \"Authorization: Bearer $KEY\" -d '{\"name\":\"Main\",\"clientId\":\"…\",\"clientSecret\":\"…\"}' — register your X app.","Open the returned authorizeUrl in a browser to finish X OAuth.","curl -X POST $API/v1/social/posts -H \"Authorization: Bearer $KEY\" -d '{\"text\":\"hello\",\"targets\":[\"x\",\"bluesky\",\"threads\"]}' — cross-post."],"useCases":["Cross-post a launch announcement to X, Bluesky, and Threads in one call.","Schedule and repurpose content from a script or CI job (headless, no browser).","Let a multi-tenant SaaS app post on behalf of its users without storing their OAuth secrets itself."],"endpoints":[{"method":"GET","path":"/v1/social/platforms","group":"Platforms","description":"List supported social platforms and their capabilities.","auth":"api_key","example":"[\n  { \"platform\": \"x\", \"authType\": \"oauth2\", \"capabilities\": { \"reply\": true, \"media\": true, \"maxTextLength\": 25000 } },\n  { \"platform\": \"bluesky\", \"authType\": \"app_password\", \"capabilities\": { \"reply\": false, \"media\": false, \"maxTextLength\": 300 } },\n  { \"platform\": \"threads\", \"authType\": \"oauth2\", \"capabilities\": { \"reply\": true, \"media\": false, \"maxTextLength\": 500 } }\n]"},{"method":"GET","path":"/v1/social/platforms/:platform/connections","group":"Connections","description":"List this account's saved connections for a platform.","auth":"api_key"},{"method":"POST","path":"/v1/social/platforms/:platform/connections","group":"Connections","description":"Register a BYOK connection (X app creds, Bluesky app password, Threads token).","auth":"api_key"},{"method":"POST","path":"/v1/social/platforms/x/connections/import","group":"Connections","description":"Import pre-obtained X OAuth tokens directly (skip the hosted authorize flow).","auth":"api_key"},{"method":"GET","path":"/v1/social/platforms/:platform/connections/:id","group":"Connections","description":"One connection by id (identity/bookkeeping only — credentials are never returned).","auth":"api_key"},{"method":"DELETE","path":"/v1/social/platforms/:platform/connections/:id","group":"Connections","description":"Delete a connection by id.","auth":"api_key"},{"method":"PATCH","path":"/v1/social/platforms/:platform/connections/:id/default","group":"Connections","description":"Mark a connection as the default for its platform.","auth":"api_key"},{"method":"GET","path":"/v1/social/platforms/x/oauth/authorize","group":"OAuth","description":"Start the X OAuth 2.0 PKCE flow. Returns { authorizeUrl } to open in a browser.","auth":"api_key"},{"method":"POST","path":"/v1/social/posts","group":"Posting","description":"Cross-post to multiple platforms in one call (default connection per target unless overridden). Pass scheduledAt (ISO 8601) to post later instead of now.","auth":"api_key","example":"{\n  \"status\": 207,\n  \"summary\": \"2 of 3 posted\",\n  \"results\": [\n    { \"platform\": \"x\", \"ok\": true, \"id\": \"1799…\" },\n    { \"platform\": \"bluesky\", \"ok\": true, \"uri\": \"at://…\" },\n    { \"platform\": \"threads\", \"ok\": false, \"error\": \"token expired\" }\n  ]\n}"},{"method":"POST","path":"/v1/social/posts/x","group":"Posting","description":"Post to X only (shortcut for a single-target X post).","auth":"api_key"},{"method":"GET","path":"/v1/social/posts","group":"Posting","description":"Post history and the pending scheduled queue (filter with ?status=scheduled).","auth":"api_key"},{"method":"DELETE","path":"/v1/social/posts/:id","group":"Posting","description":"Cancel a scheduled post that hasn't dispatched yet (409 once it has).","auth":"api_key"},{"method":"GET","path":"/v1/social/best-times","group":"Intelligence","description":"Curated best-times-to-post windows per platform + ready-made slot suggestions.","auth":"api_key"},{"method":"POST","path":"/v1/social/compose","group":"Intelligence","description":"Generate post/reply drafts in one of your registered voices (fits X's weighted 280).","auth":"api_key"},{"method":"PUT","path":"/v1/social/voices/:slug","group":"Voices","description":"Register or update a voice profile — the style guide compose writes in.","auth":"api_key"},{"method":"GET","path":"/v1/social/voices","group":"Voices","description":"List your voice profiles.","auth":"api_key"},{"method":"GET","path":"/v1/social/voices/:slug","group":"Voices","description":"One voice profile, including its full guidelines.","auth":"api_key"},{"method":"DELETE","path":"/v1/social/voices/:slug","group":"Voices","description":"Delete a voice profile.","auth":"api_key"},{"method":"POST","path":"/v1/social/watches","group":"Listening","description":"Watch mentions, keywords, or an account; found posts land in your inbox. Reply modes: off / queue / auto.","auth":"api_key"},{"method":"GET","path":"/v1/social/watches","group":"Listening","description":"List your watches.","auth":"api_key"},{"method":"PATCH","path":"/v1/social/watches/:id","group":"Listening","description":"Update a watch (enable/disable, query, reply mode, voice, caps).","auth":"api_key"},{"method":"DELETE","path":"/v1/social/watches/:id","group":"Listening","description":"Delete a watch.","auth":"api_key"},{"method":"GET","path":"/v1/social/inbox","group":"Listening","description":"Posts your watches found, with any drafted replies (filter by status).","auth":"api_key"},{"method":"POST","path":"/v1/social/inbox/:id/reply","group":"Listening","description":"Send the drafted (or provided) reply to a found post.","auth":"api_key"},{"method":"POST","path":"/v1/social/inbox/:id/skip","group":"Listening","description":"Dismiss a found post without replying.","auth":"api_key"},{"method":"GET","path":"/v1/social/listen-settings","group":"Listening","description":"Your monthly X read budget and how much of it this month's polling has used.","auth":"api_key"},{"method":"PUT","path":"/v1/social/listen-settings","group":"Listening","description":"Set the monthly X read budget (X bills ~$0.005 per post read; pollers stop at the cap).","auth":"api_key"}],"cliCommands":["social list","social add <x|bluesky|threads>","social remove <name-or-id>","social default <name-or-id>"],"enabled":true},{"id":"assistant","name":"Assistant","tagline":"A read-only AI helper for finding feeds, your subscriptions, and the cheapass CLI.","description":"An LLM-backed assistant that searches the cheapass catalog and answers from product docs. It never mutates state: when something is actionable, it returns the literal `cheapass …` command(s) for the user to run.","pitch":"A read-only AI helper that finds feeds and tells you which `cheapass` command to run. It can't touch your account or spend your money, which makes it the most trustworthy thing we ship.","basePath":"/v1/assistant","category":"build","status":"available","alwaysOn":true,"howItWorks":"Send a chat history to POST /v1/assistant/chat (server-sent events stream). The assistant has tools to search the feeds catalog and read your subscriptions; when a request is actionable it returns `cheapass <product> <subcommand>` commands (each with a one-line description) for you to paste into a terminal. The TUI is invoked by running `cheapass` with no arguments. The only state-changing exception is `login`, which mounts the real device-flow Login inline.","getStarted":["cheapass — open the assistant TUI.","cheapass ask \"how do I subscribe to the BBC news feed?\" — one-shot question from the shell.","Inside the TUI, type `feeds categories` or `?how do I post to X` — recognized commands run directly, anything else goes to the assistant."],"useCases":["Discover feeds by topic without memorizing CLI subcommands.","Get copy-pasteable `cheapass …` commands for whatever you're trying to do.","Ask reference questions about endpoints, auth, or pricing from inside the terminal."],"endpoints":[{"method":"POST","path":"/v1/assistant/chat","description":"Stream an assistant reply over SSE. Accepts a chat history.","auth":"api_key","example":"data: {\"type\":\"tool\",\"name\":\"search_catalog\",\"phase\":\"call\"}\ndata: {\"type\":\"tool\",\"name\":\"search_catalog\",\"phase\":\"result\"}\ndata: {\"type\":\"text\",\"text\":\"To subscribe to BBC News, run: \"}\ndata: {\"type\":\"text\",\"text\":\"cheapass feeds subscribe https://feeds.bbci.co.uk/news/rss.xml\"}"},{"method":"GET","path":"/v1/usage","group":"Account","description":"Your usage over the headless API: request counts, error rate, and latency (avg/p95) from the per-request log — scoped to the calling key's project by default (?project=<id> overrides, ?hours= sets the window) — plus assistant token usage and today's budget.","auth":"api_key"}],"cliCommands":["(none) — open the TUI with bare `cheapass`, or use `cheapass ask \"<question>\"`"],"enabled":true},{"id":"feeds","name":"Feeds","tagline":"Curated RSS/Atom reader. Each source is fetched once globally; reads are fast and deduped.","description":"Subscribe to a curated catalog of shared upstream RSS/Atom feeds (or any URL — validated and SSRF-guarded, with feed autodiscovery). Every unique feed URL is fetched once globally on a schedule; accounts hold a lightweight subscription pointer plus per-user read/starred/tags state.","pitch":"A curated RSS/Atom reader. ~300 sources, each fetched once globally and shared, so a thousand of you subscribing to the BBC is still one request to the BBC. Full titles, snippets, whole articles, images, authors, categories, and dates — everything the source hands over, in one shape.","basePath":"/v1/feeds","category":"ingest","status":"available","alwaysOn":false,"howItWorks":"A scheduled job ticks hourly and, per feed, fetches each unique feed URL once (respecting per-feed cadence and ETag/Last-Modified), parses items into a shared store, and updates the feed's status (active/errored/dead) and reliability. Your account holds only subscription pointers and per-(user, item) state — never a copy of the feed payload. GET /v1/feeds/items merges your subscriptions' items with your read/starred/tags state, newest first. Because upstreams are shared, N subscribers to the same feed cause exactly one outbound fetch. Retention: a daily job keeps roughly the last 30 days per feed (and always the newest 50 items, so low-volume sources never go empty); older items beyond that are purged.","getStarted":["cheapass login — authenticate the CLI.","cheapass feeds categories — browse the curated catalog.","cheapass feeds subscribe <url> — subscribe (URL is validated; feed autodiscovery runs automatically).","cheapass feeds items --unread — read your stream interactively.","curl \"$API/v1/feeds/items?format=rss\" -H \"Authorization: Bearer $KEY\" — your merged stream as an RSS feed (also ?format=atom).","curl \"$API/v1/feeds/items?format=csv\" -H \"Authorization: Bearer $KEY\" — the same items as CSV for a spreadsheet.","curl \"$API/v1/feeds/sources?format=csv\" — the whole source catalog as CSV (public, no key)."],"useCases":["Read news across publishers by topic, with the same items deduped across N users.","Subscribe a team to the same feeds without N× the outbound fetches.","Layer your own read/starred/tags on top of a shared global catalog."],"endpoints":[{"method":"GET","path":"/v1/feeds/categories","group":"Catalog","description":"Curated category taxonomy, grouped into sections (parentName) with a feed count each. Public, no API key. JSON (default) or CSV via ?format=csv.","auth":"none"},{"method":"GET","path":"/v1/feeds/categories/:slug/feeds","group":"Catalog","description":"Feeds in a category (each with `reliability`).","auth":"api_key"},{"method":"GET","path":"/v1/feeds/subscriptions","group":"Subscriptions","description":"List your subscriptions.","auth":"api_key"},{"method":"POST","path":"/v1/feeds/subscriptions","group":"Subscriptions","description":"Subscribe to a feed URL. Validated, SSRF-guarded, with feed autodiscovery. Optionally place it in a folder.","auth":"api_key"},{"method":"GET","path":"/v1/feeds/subscriptions/:id","group":"Subscriptions","description":"One subscription by id.","auth":"api_key"},{"method":"PATCH","path":"/v1/feeds/subscriptions/:id","group":"Subscriptions","description":"Rename a subscription and/or move it to a folder. `folderId: null` removes it from its folder.","auth":"api_key"},{"method":"DELETE","path":"/v1/feeds/subscriptions/:id","group":"Subscriptions","description":"Unsubscribe by subscription id.","auth":"api_key"},{"method":"GET","path":"/v1/feeds/items","group":"Reading","description":"Aggregated stream across your subscriptions, merged with your per-user state. One comprehensive method: every item returns everything the source exposes — full title, snippet, full article HTML and plain text, all images, categories, every author, source attribution, and both published & updated timestamps. Missing fields are null/[] (not omitted). Available in four formats: JSON (default), CSV for spreadsheets/ETL, and RSS or Atom so the merged stream can be re-subscribed in any reader — pass ?format=json|csv|rss|atom or the matching Accept header.","auth":"api_key","example":"{\n  \"id\": \"k57…\",\n  \"source\": { \"title\": \"AI | The Verge\", \"url\": \"https://www.theverge.com/rss/ai-artificial-intelligence/index.xml\", \"language\": \"en-US\" },\n  \"title\": \"The film about Sam Altman has been dropped by Amazon MGM\",\n  \"url\": \"https://www.theverge.com/…/sam-altman-film\",\n  \"authors\": [\"Hayden Field\"],\n  \"categories\": [\"AI\", \"News\", \"OpenAI\"],\n  \"publishedAt\": \"2026-06-19T14:15:29.000Z\",\n  \"updatedAt\": \"2026-06-19T14:58:45.000Z\",\n  \"summary\": \"Luca Guadagnino's film about OpenAI CEO Sam Altman…\",\n  \"contentHtml\": \"<p>…full article body…</p>\",\n  \"contentText\": \"…full article body, tags stripped…\",\n  \"image\": \"https://platform.theverge.com/…/STK201_SAM_ALTMAN.jpg\",\n  \"images\": [{ \"url\": \"https://platform.theverge.com/…jpg\", \"type\": \"image/jpeg\" }],\n  \"read\": false, \"starred\": false, \"tags\": []\n}"},{"method":"GET","path":"/v1/feeds/sources","group":"Catalog","description":"Every source in the catalog (public, no API key). Browse what's available and which categories each belongs to. JSON (default) or CSV via ?format=csv.","auth":"none"},{"method":"GET","path":"/v1/feeds/sources/:id/example","group":"Catalog","description":"Public, no API key. One real recent item from a source in the full normalized shape, plus a `fields` map showing exactly which data that source provides. Powers the per-source examples in these docs.","auth":"none"},{"method":"GET","path":"/v1/feeds/items/:id","group":"Reading","description":"One item by id, in the full normalized shape, merged with your read/starred/tags state.","auth":"api_key"},{"method":"PATCH","path":"/v1/feeds/items/:id","group":"Reading","description":"Set read/starred/tags on an item (send `false` to mark unread / un-star).","auth":"api_key"},{"method":"GET","path":"/v1/feeds/folders","group":"Folders","description":"List your folders, each with its subscription count.","auth":"api_key"},{"method":"POST","path":"/v1/feeds/folders","group":"Folders","description":"Create a folder to organize subscriptions.","auth":"api_key"},{"method":"GET","path":"/v1/feeds/folders/:id","group":"Folders","description":"One folder by id.","auth":"api_key"},{"method":"PATCH","path":"/v1/feeds/folders/:id","group":"Folders","description":"Rename a folder.","auth":"api_key"},{"method":"DELETE","path":"/v1/feeds/folders/:id","group":"Folders","description":"Delete a folder. Subscriptions in it are kept and moved out of the folder.","auth":"api_key"}],"cliCommands":["feeds categories","feeds sources","feeds browse <category>","feeds subscribe <url>","feeds list","feeds items [--unread] [--json]","feeds unsubscribe <id>","feeds read <id>","feeds star <id>"],"enabled":false},{"id":"connect","name":"Connect","tagline":"A reusable OAuth connection vault. Authenticate once, request scoped tokens from any project.","description":"Bring your own OAuth app for GitHub, Google, LinkedIn, X, Discord, or Spotify. cheapass stores the connection encrypted and never returns the refresh token. Mint short-lived scoped access tokens at runtime from any project, so your apps never handle refresh-token storage themselves.","pitch":"A reusable OAuth connection vault. Authenticate to GitHub, Google, LinkedIn, X, or Discord once, then mint scoped runtime tokens from any project. Bring your own OAuth app; we hold the credentials encrypted and never hand back the refresh token.","basePath":"/v1/connect","category":"build","highlights":[{"label":"Providers","items":["GitHub","Google","LinkedIn","X","Discord","Spotify"]}],"status":"available","alwaysOn":false,"minPlan":"junior","howItWorks":"You bring your own OAuth app credentials for a supported provider and POST them to /v1/connect/connections along with the scopes you want authorized. cheapass runs the OAuth flow (PKCE where the provider supports it), stores the resulting grant (access/refresh/expiry/scopes) AES-256-GCM encrypted, and never returns the refresh token on read. When your project needs to call the upstream API, POST /v1/connect/tokens to mint a fresh access token; cheapass refreshes it first if it's stale. The CLI `cheapass connect …` mirrors every action and starts a loopback HTTP listener so the terminal learns instantly that OAuth completed.","getStarted":["cheapass login — authenticate the CLI.","cheapass connect providers — see supported providers and required scopes.","cheapass connect add github — interactive OAuth flow (opens a browser).","cheapass connect token github — mint a fresh access token for use in your project."],"useCases":["Centralize GitHub/Google/LinkedIn OAuth across multiple side projects without each one handling refresh-token storage.","Hand scoped, short-lived tokens to a CI job or serverless function from one vault.","Rotate or revoke a provider connection in one place without touching each consumer."],"endpoints":[{"method":"GET","path":"/v1/connect/providers","group":"Providers","description":"List supported OAuth providers and their capabilities.","auth":"api_key"},{"method":"GET","path":"/v1/connect/connections","group":"Connections","description":"List your saved connections.","auth":"api_key"},{"method":"POST","path":"/v1/connect/connections","group":"Connections","description":"Register a new BYOK OAuth app + start the OAuth flow.","auth":"api_key"},{"method":"GET","path":"/v1/connect/connections/:id","group":"Connections","description":"Get one connection (refresh token is never returned).","auth":"api_key"},{"method":"DELETE","path":"/v1/connect/connections/:id","group":"Connections","description":"Delete a connection.","auth":"api_key"},{"method":"PATCH","path":"/v1/connect/connections/:id/default","group":"Connections","description":"Mark a connection as the default for its provider.","auth":"api_key"},{"method":"POST","path":"/v1/connect/tokens","group":"Tokens","description":"Mint a fresh access token for a connection (refreshes first if stale).","auth":"api_key","example":"{\n  \"accessToken\": \"gho_xxxxxxxxxxxxxxxxxxxx\",\n  \"expiresAt\": \"2026-06-20T01:30:00.000Z\",\n  \"scopes\": [\"repo\", \"read:user\"]\n}"}],"cliCommands":["connect providers","connect list [--provider p]","connect add <provider>","connect remove <id>","connect default <id>","connect token <provider>"],"enabled":false},{"id":"rss","name":"RSS","tagline":"Fetch, discover, convert, merge, and monitor feeds — anything in, normalized items out.","description":"A toolkit for working with feeds programmatically (distinct from the curated Feeds reader). Fetch & parse any feed with delta cursors, auto-discover feeds on a page, turn feedless sites and YouTube/Reddit/GitHub/Substack into feeds, merge and dedupe across sources, and get webhook push on new items so you can stop polling. Every endpoint returns the same normalized item shape as /v1/feeds/items.","pitch":"Fetch with delta cursors, auto-discover feeds on any page, turn feedless sites into RSS, convert YouTube, Reddit, GitHub, and Substack into feeds, merge and dedupe across sources, and get a webhook on new items so you can stop polling.","basePath":"/v1/rss","category":"publish","status":"available","alwaysOn":false,"howItWorks":"rss/fetch and the from-* converters parse an upstream into the canonical normalized item shape (title, url, authors[], categories[], publishedAt/updatedAt, summary, contentHtml/contentText, image, images[]) — identical to GET /v1/feeds/items so one consumer shape covers both products. rss/fetch accepts a `since` cursor and returns only newer items plus a fresh cursor (delta polling). rss/discover scrapes <link rel=alternate type=application/rss+xml> and common feed paths from a page. rss/from-site renders a feedless page (Readability/Cheerio, headless when needed) and synthesizes items from repeated article blocks. rss/from-youtube|reddit|github|substack map those public endpoints to items. rss/merge fetches N feeds concurrently, dedupes by guid+url, and returns one time-ordered stream. rss/monitor registers a webhook that is POSTed new items as a background worker discovers them — at-least-once delivery. rss/feeds turns any of these sources into a PERSISTENT, subscribable feed: POST a source once and get a stable GET /v1/rss/feeds/:id.xml URL (public, no key) that any feed reader can poll — re-synthesized on read and cached for ~10 minutes. Hosted feeds are PRIVATE (unlisted) by default — the URL works for anyone who has it, but the feed is listed nowhere until you set it public, when it joins /v1/rss/directory. from-site needs the headless browser (the same Cloudflare Browser Rendering the web product uses); the rest are pure fetch+parse.","getStarted":["cheapass login","cheapass products enable rss","curl -X POST $API/v1/rss/fetch -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://news.ycombinator.com/rss\"}' — parse any feed.","curl -X POST $API/v1/rss/from-youtube -H \"Authorization: Bearer $KEY\" -d '{\"handle\":\"@mkbhd\"}' — turn a channel into a feed.","curl -X POST $API/v1/rss/feeds -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://example.com/blog\",\"selector\":\".post\"}' — get a subscribable feed URL for a feedless site."],"useCases":["Poll a feed efficiently with delta cursors instead of re-downloading the whole thing.","Turn a site with no feed (or YouTube/Reddit/GitHub/Substack) into a normalized feed.","Merge a dozen sources into one deduped stream, or get webhook push on new items.","Give a feedless site a real, subscribable RSS URL your reader can poll — created once, hosted for you."],"endpoints":[{"method":"POST","path":"/v1/rss/fetch","group":"Converters","description":"Fetch & parse any feed URL into normalized items. Pass a `since` cursor for delta polling (only newer items).","auth":"api_key","example":"{\n  \"items\": [\n    { \"title\": \"…\", \"url\": \"https://…\", \"authors\": [\"…\"], \"categories\": [\"…\"],\n      \"publishedAt\": \"2026-06-19T14:15:29.000Z\", \"summary\": \"…\",\n      \"contentHtml\": \"<p>…</p>\", \"contentText\": \"…\", \"image\": \"https://…\", \"images\": [{ \"url\": \"https://…\" }] }\n  ],\n  \"cursor\": \"2026-06-19T14:15:29.000Z\"\n}"},{"method":"POST","path":"/v1/rss/discover","group":"Converters","description":"Auto-discover feed URLs advertised on (or conventionally located at) a site URL.","auth":"api_key"},{"method":"POST","path":"/v1/rss/from-site","group":"Converters","description":"Synthesize a feed from a feedless page by extracting repeated article blocks (headless render when needed).","auth":"api_key"},{"method":"POST","path":"/v1/rss/from-youtube","group":"Converters","description":"Turn a YouTube channel or playlist into a normalized feed.","auth":"api_key"},{"method":"POST","path":"/v1/rss/from-reddit","group":"Converters","description":"Turn a subreddit into a normalized feed.","auth":"api_key"},{"method":"POST","path":"/v1/rss/from-github","group":"Converters","description":"Turn a GitHub repo's releases into a normalized feed.","auth":"api_key"},{"method":"POST","path":"/v1/rss/from-substack","group":"Converters","description":"Turn a Substack publication into a normalized feed.","auth":"api_key"},{"method":"POST","path":"/v1/rss/merge","group":"Converters","description":"Fetch multiple feeds, dedupe by guid+url, and return one time-ordered stream.","auth":"api_key"},{"method":"POST","path":"/v1/rss/monitor","group":"Monitors","description":"Register a webhook that is POSTed new items as they appear (stop polling). At-least-once delivery.","auth":"api_key"},{"method":"GET","path":"/v1/rss/monitor","group":"Monitors","description":"List your active monitors.","auth":"api_key"},{"method":"DELETE","path":"/v1/rss/monitor/:id","group":"Monitors","description":"Stop and delete a monitor.","auth":"api_key"},{"method":"POST","path":"/v1/rss/feeds","group":"Hosted feeds","description":"Create a persistent hosted feed you can subscribe to in any reader, from any source (a feedless site + selector, a YouTube channel, a merge set, …). Returns a stable feed URL. Kind is inferred from the fields, or pass it explicitly.","auth":"api_key","example":"{ \"id\": \"k17…\", \"kind\": \"site\", \"title\": \"example.com\", \"feedUrl\": \"https://api.cheapass.dev/v1/rss/feeds/k17….xml\", \"itemCount\": 20 }"},{"method":"GET","path":"/v1/rss/feeds","group":"Hosted feeds","description":"List your hosted feeds.","auth":"api_key"},{"method":"GET","path":"/v1/rss/feeds/:id.xml","group":"Hosted feeds","description":"The hosted feed itself — PUBLIC (no API key); subscribe to it in any reader. A readable /:id/:slug.xml form works too (the id resolves; the slug is cosmetic). Re-synthesized on read, cached ~10 min. Append .atom for Atom.","auth":"none"},{"method":"GET","path":"/v1/rss/directory","group":"Discover","description":"Public directory of feeds users have opted to share (visibility: public). Filter with ?category=. Feeds are private (unlisted) by default and never appear here unless shared.","auth":"none"},{"method":"DELETE","path":"/v1/rss/feeds/:id","group":"Hosted feeds","description":"Delete a hosted feed (its public URL stops working).","auth":"api_key"}],"cliCommands":["rss fetch <url> [--since <cursor>]","rss discover <url>","rss from-youtube <channel>","rss from-reddit <subreddit>","rss merge <url>...","rss monitor add <url> --webhook <url>","rss monitor list","rss feeds create <url> [--selector <css>]","rss feeds list"],"enabled":false},{"id":"web","name":"Web","tagline":"URL in, clean data out — markdown for RAG, metadata, a real headless browser, change monitoring.","description":"Turn any URL into clean, structured data. Readability-extracted markdown for RAG and AI pipelines, Open Graph / canonical metadata, real headless-browser rendering for JS-heavy pages (rendered HTML, markdown, CSS-selector scrapes, screenshots), and webhook-pushed change monitoring.","pitch":"URL → clean markdown for RAG. Open Graph metadata and canonical resolution. A real headless browser (Cloudflare Browser Rendering) for JS-heavy pages — render, screenshot, scrape. URL change monitoring with webhook push.","basePath":"/v1/web","category":"ingest","status":"available","alwaysOn":false,"howItWorks":"All four endpoints are live. web/markdown fetches a URL and runs a dependency-free, in-isolate extractor — it strips scripts/nav/ads, isolates the main <article>/<main>, and converts a curated subset of tags to markdown (returns title, byline, excerpt, lang, wordCount). It's a pragmatic heuristic, not a full Mozilla Readability pass (the V8 isolate can't host jsdom), but it's reliable for ordinary articles and needs no external service. web/metadata parses <meta>/OG/Twitter tags and resolves the canonical URL + favicon by string-parsing the head — no JS execution. web/browser is the heavy one: it drives a real headless Chromium via Cloudflare Browser Rendering, so it executes the page's JavaScript, waits for a selector or the network to settle, then returns rendered HTML, Cloudflare-generated markdown, CSS-selector scrapes, and/or a PNG screenshot (served from storage as a URL) — whatever combination you ask for, run concurrently. Use it for single-page apps and JS-gated content that web/markdown (which never runs JS) can't see. web/monitor stores a per-tenant watch on a URL; a cron re-fetches it every interval, hashes the extracted text (optionally scoped to a CSS selector — #id, .class, or tag), and POSTs your webhook when the hash changes. All fetches are SSRF-guarded (no private/loopback addresses), including your webhook and render URLs. Honest note: web/browser is a general-purpose renderer — you supply the URL, so respecting the target site's terms of service is on you, not us. We run the browser; we don't vet what you point it at.","getStarted":["curl -X POST $API/v1/web/markdown -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://example.com/article\"}' — clean article markdown.","curl -X POST $API/v1/web/metadata -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://example.com\"}' — OG/Twitter/canonical/favicon.","curl -X POST $API/v1/web/browser -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://example.com/app\",\"render\":\"html\",\"waitFor\":\".loaded\"}' — render a JS app after its content appears.","curl -X POST $API/v1/web/browser -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://example.com\",\"scrape\":[\".price\",\".title\"],\"screenshot\":true}' — scrape selectors + grab a screenshot in one call.","curl -X POST $API/v1/web/monitor -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://example.com/pricing\",\"webhookUrl\":\"https://you.dev/hook\"}' — get pinged when it changes."],"useCases":["Feed clean article markdown into a RAG pipeline without writing a scraper.","Render and scrape a JS-heavy SPA that has no API — wait for content, pull selectors, get structured elements back.","Screenshot a page as it actually renders in a real browser.","Get notified when a pricing page or docs page changes."],"endpoints":[{"method":"POST","path":"/v1/web/markdown","description":"Extract the main article and return clean markdown (in-isolate heuristic extractor — strips boilerplate, isolates <article>/<main>, converts to markdown).","auth":"api_key","example":"{\n  \"title\": \"The article title\",\n  \"byline\": \"Jane Doe\",\n  \"lang\": \"en\",\n  \"wordCount\": 812,\n  \"markdown\": \"# The article title\\n\\nClean body text as markdown…\"\n}"},{"method":"POST","path":"/v1/web/metadata","description":"Open Graph / Twitter / meta tags + canonical URL and favicon, no JS execution.","auth":"api_key"},{"method":"POST","path":"/v1/web/browser","description":"Headless-render a JS-heavy page in a real Chromium (Cloudflare Browser Rendering): executes JavaScript, waits for a selector or the network to settle, and returns any combination of rendered HTML, markdown, CSS-selector scrapes, and a PNG screenshot (returned as a Convex storage URL — the response is always JSON). You supply the URL; respecting the target site's ToS is the caller's responsibility.","auth":"api_key","example":"{\n  \"url\": \"https://example.com/app\",\n  \"markdown\": \"# Rendered after JS ran\\n\\n…\",\n  \"elements\": [{ \"selector\": \".price\", \"results\": [{ \"text\": \"$9\", \"html\": \"<span…\", \"attributes\": [] }] }],\n  \"screenshotUrl\": \"https://…/storage/…png\"\n}"},{"method":"POST","path":"/v1/web/monitor","description":"Watch a URL (optionally scoped to a CSS selector — #id, .class, or tag) for changes and POST a webhook when its text changes. intervalSeconds clamps to 300–86400.","auth":"api_key"},{"method":"GET","path":"/v1/web/monitor","description":"List your active URL monitors.","auth":"api_key"},{"method":"DELETE","path":"/v1/web/monitor/:id","description":"Stop and delete a URL monitor.","auth":"api_key"}],"cliCommands":["web markdown <url> [--out file.md]","web metadata <url>","web browser <url> [--html|--markdown] [--screenshot] [--scrape .sel]... [--wait-for .sel]","web monitor add <url> --webhook <url> [--selector .price] [--interval 3600]","web monitor list","web monitor remove <id>"],"enabled":false},{"id":"text","name":"Text","tagline":"Cheap text utilities — summarize, extract entities, classify, detect language & sentiment.","description":"Small, fast, cheap text endpoints backed by open models. Summarize articles, extract named entities, zero-shot classify by your own labels, detect language and sentiment, and compute reading time. No image-gen drama — just text in, structured text out.","pitch":"Summarize, extract named entities, classify against your own labels, detect language and sentiment, compute reading time. Small open models, cheap calls. No image-gen drama.","basePath":"/v1/text","category":"build","status":"coming_soon","alwaysOn":false,"howItWorks":"LLM-backed endpoints (summarize, classify) route to a fast open-model provider (Groq / Together / Fireworks) behind a tight system prompt; entities uses a purpose-built NER model (GLiNER) for accuracy and cost at scale; classify is zero-shot against the labels you pass; language and reading-time run as local libraries (no upstream cost); sentiment uses a small fine-tuned model. Each endpoint accepts either raw `text` or a `url` (fetched + cleaned via web/markdown first).","getStarted":["(Not yet available — these endpoints return 501 not_implemented today.)","When shipped: cheapass products enable text.","curl -X POST $API/v1/text/summarize -H \"Authorization: Bearer $KEY\" -d '{\"url\":\"https://…\",\"maxWords\":80}' — summarize an article."],"useCases":["Summarize long articles or feed items down to a tweet-length blurb.","Extract people / orgs / places from text for tagging or search.","Classify incoming messages into your own categories with zero training."],"endpoints":[{"method":"POST","path":"/v1/text/summarize","description":"Summarize text or a URL to a target length.","auth":"api_key","example":"{ \"summary\": \"A concise 2-3 sentence summary of the input…\", \"wordCount\": 42 }"},{"method":"POST","path":"/v1/text/entities","description":"Named-entity recognition — people, orgs, places, products, dates.","auth":"api_key","example":"{ \"entities\": [\n  { \"text\": \"Sam Altman\", \"type\": \"PERSON\", \"score\": 0.99 },\n  { \"text\": \"OpenAI\", \"type\": \"ORG\", \"score\": 0.98 }\n] }"},{"method":"POST","path":"/v1/text/classify","description":"Zero-shot classification against labels you provide.","auth":"api_key"},{"method":"POST","path":"/v1/text/language","description":"Detect the language of a piece of text.","auth":"api_key"},{"method":"POST","path":"/v1/text/sentiment","description":"Sentiment polarity of a piece of text.","auth":"api_key"},{"method":"POST","path":"/v1/text/reading-time","description":"Word count and estimated reading time.","auth":"api_key"}],"cliCommands":["text summarize <file|url>","text entities <file>","text classify <file> --labels a,b,c","text sentiment <file>"],"enabled":false},{"id":"search","name":"Search","tagline":"Web and news search through one API, plus trending detection across your feeds.","description":"A thin, commercial-friendly proxy over a web-search backend (Brave / Serper / Tavily) for web and news results, plus an in-house trending endpoint that surfaces what's rising across your subscribed feed bundles.","pitch":"Web and news search through one stable shape, plus trending detection across your subscribed feeds. We proxy a real search vendor so you don't have to pick one or marry it.","basePath":"/v1/search","category":"ingest","status":"coming_soon","alwaysOn":false,"howItWorks":"search/web and search/news proxy a single configured upstream (Brave Search recommended) and normalize results to a stable shape, so you never bind to a vendor. search/trending is in-house: it indexes recent items from your subscribed feeds (Feeds product) and ranks terms by TF-IDF over a recent window against a baseline window, returning rising topics with their backing items.","getStarted":["(Not yet available — these endpoints return 501 not_implemented today.)","When shipped: cheapass products enable search.","curl \"$API/v1/search/news?q=openai&freshness=day\" -H \"Authorization: Bearer $KEY\" — recent news."],"useCases":["Add web/news search to an app without signing up with a search vendor directly.","Surface trending topics across the feeds your users follow.","Vendor-neutral search: swap the upstream without changing your integration."],"endpoints":[{"method":"GET","path":"/v1/search/web","description":"Web search. Normalized results regardless of the upstream provider.","auth":"api_key","example":"{\n  \"query\": \"best espresso machine\",\n  \"results\": [\n    { \"title\": \"The 8 Best Espresso Machines of 2026\", \"url\": \"https://example.com/best-espresso\", \"description\": \"…\" }\n  ]\n}"},{"method":"GET","path":"/v1/search/news","description":"News search with recency filtering.","auth":"api_key"},{"method":"GET","path":"/v1/search/trending","description":"Trending topics across your subscribed feed bundles (in-house).","auth":"api_key"}],"cliCommands":["search web <query>","search news <query> [--freshness day]","search trending"],"enabled":false},{"id":"data","name":"Data","tagline":"One API over dozens of free public datasets — weather, finance, health, drugs, gov, science, and more.","description":"A unified, normalized proxy over free and commercial-friendly public datasets: weather & air quality; forex/crypto/stocks/economic indicators; US government (treasury, SEC, census, spending); nutrition & fitness (USDA FoodData Central, exercises); health & medicine (drug labels, RxNorm, clinical trials, chemical/compound data, NIH research grants, preventive-health guidance); and science/reference (NASA, Wikipedia, books, music, dictionary). One key, one shape per domain, sensible caching — so you don't sign up for forty providers.","pitch":"Thirty-odd public datasets behind one key — weather, forex, crypto, SEC filings, earthquakes, the national debt, NASA, Wikipedia. Most are free government APIs. We normalize and cache them so you never have to read their documentation.","basePath":"/v1/data","category":"ingest","status":"coming_soon","alwaysOn":false,"howItWorks":"Status: planned — endpoints return 501 until shipped. Each endpoint proxies one upstream (named below), normalizes the response to a stable shape, and caches aggressively to respect upstream rate limits (per the shared-upstream principle: fetch once globally, serve from cache). Upstreams that require attribution are attributed in responses. Source of truth for upstreams + licensing: _docs/cheapass-upstreams-reference.md.","getStarted":["(Not yet available — these endpoints return 501 not_implemented today.)","When shipped: cheapass products enable data.","curl \"$API/v1/data/weather?lat=42.36&lng=-71.06\" -H \"Authorization: Bearer $KEY\" — current + forecast."],"useCases":["Pull weather, exchange rates, or crypto prices without managing N provider accounts.","Build a nutrition, fitness, or medication app on USDA food data, drug labels, and RxNorm.","Power health/research tools with clinical trials, PubChem compounds, and NIH grant data.","Build civic/finance tools on US government open data (SEC, FRED, census, spending)."],"endpoints":[{"method":"GET","path":"/v1/data/weather","group":"Weather & earth","auth":"api_key","billing":"free_tier_only","description":"Current conditions + daily forecast by coordinates (Open-Meteo).","example":"{ \"current\": { \"temperature_2m\": 18.2, \"wind_speed_10m\": 11.0 }, \"daily\": [{ \"date\": \"2026-06-20\", \"tMax\": 24, \"tMin\": 14 }], \"units\": { \"temperature_2m\": \"°C\" } }"},{"method":"GET","path":"/v1/data/air-quality","group":"Weather & earth","auth":"api_key","billing":"free_tier_only","description":"AQI, PM2.5/PM10, ozone, NO2, CO, SO2 by coordinates (Open-Meteo Air Quality)."},{"method":"GET","path":"/v1/data/sunrise-sunset","group":"Weather & earth","auth":"api_key","description":"Sunrise, sunset, and twilight times by coordinates/date (sunrise-sunset.org)."},{"method":"GET","path":"/v1/data/tides","group":"Weather & earth","auth":"api_key","description":"Tide predictions for US coastal stations (NOAA CO-OPS)."},{"method":"GET","path":"/v1/data/earthquakes","group":"Weather & earth","auth":"api_key","description":"Earthquake event query with magnitude/time/area filters (USGS)."},{"method":"GET","path":"/v1/data/forex","group":"Money & markets","auth":"api_key","description":"Foreign-exchange reference rates (ExchangeRate-API open access)."},{"method":"GET","path":"/v1/data/crypto","comingSoon":true,"group":"Money & markets","auth":"api_key","description":"Crypto market data — price, market cap, 24h volume/change (CoinGecko). (Coming soon — returns 501; not wired up yet.)"},{"method":"GET","path":"/v1/data/stocks","comingSoon":true,"group":"Money & markets","auth":"api_key","description":"Stock quotes and time series (Alpha Vantage). (Coming soon — returns 501; needs an Alpha Vantage key.)"},{"method":"GET","path":"/v1/data/economic","comingSoon":true,"group":"Money & markets","auth":"api_key","description":"Economic time series — CPI, unemployment, GDP, rates, M2 (FRED). (Coming soon — returns 501; needs a FRED key.)"},{"method":"GET","path":"/v1/data/world-bank","group":"Money & markets","auth":"api_key","description":"Country-level development indicators (World Bank Open Data)."},{"method":"GET","path":"/v1/data/treasury","group":"US government","auth":"api_key","description":"US Treasury fiscal data — national debt, exchange rates, gift contributions (Treasury Fiscal Data)."},{"method":"GET","path":"/v1/data/sec-companyfacts","group":"US government","auth":"api_key","description":"Public-company financials via XBRL, by CIK or ticker (SEC EDGAR)."},{"method":"GET","path":"/v1/data/census","comingSoon":true,"group":"US government","auth":"api_key","description":"US Census demographics by geography (Census Bureau ACS). (Coming soon — returns 501; needs a data.gov key.)"},{"method":"GET","path":"/v1/data/usaspending","group":"US government","auth":"api_key","description":"Federal contracts and grants search (USAspending.gov)."},{"method":"GET","path":"/v1/data/federal-register","group":"US government","auth":"api_key","description":"US regulations, executive orders, and notices (Federal Register)."},{"method":"GET","path":"/v1/data/fec","comingSoon":true,"group":"US government","auth":"api_key","description":"Campaign finance — candidate filings, PACs, contributions (FEC). (Coming soon — returns 501; needs a data.gov key.)"},{"method":"GET","path":"/v1/data/patents","comingSoon":true,"group":"US government","auth":"api_key","description":"Patent, trademark, and assignment search (USPTO). (Coming soon — returns 501; no integration started yet.)"},{"method":"GET","path":"/v1/data/fbi-wanted","group":"US government","auth":"api_key","description":"FBI Wanted persons list (FBI)."},{"method":"GET","path":"/v1/data/nutrition","comingSoon":true,"group":"Nutrition & fitness","auth":"api_key","description":"Authoritative food & nutrient data — search foods, get full nutrient profiles per serving (USDA FoodData Central, public domain). (Coming soon — returns 501; needs a USDA key.)"},{"method":"GET","path":"/v1/data/food-products","group":"Nutrition & fitness","auth":"api_key","description":"Packaged-product lookup — nutrition, ingredients, allergens by barcode (Open Food Facts)."},{"method":"GET","path":"/v1/data/exercises","group":"Nutrition & fitness","auth":"api_key","billing":"free_tier_only","description":"Exercise database — name, category, equipment, muscles worked (wger; CC-BY-SA, attributed). Friendly names or numeric wger IDs both accepted for category/muscle/equipment."},{"method":"GET","path":"/v1/data/drugs","group":"Health & medicine","auth":"api_key","description":"Drug labeling — dosage, warnings, ingredients, NDC info (openFDA drug label + NDC directory)."},{"method":"GET","path":"/v1/data/rxnorm","group":"Health & medicine","auth":"api_key","description":"Normalize a drug name to its RxNorm concept (RxCUI) (NLM RxNav). Name resolution, not interaction checking."},{"method":"GET","path":"/v1/data/clinical-trials","group":"Health & medicine","auth":"api_key","description":"Search clinical studies — conditions, interventions, status, sponsors (ClinicalTrials.gov v2, public domain)."},{"method":"GET","path":"/v1/data/chemicals","group":"Health & medicine","auth":"api_key","description":"Compound & chemical data — structure, properties, identifiers (PubChem PUG REST)."},{"method":"GET","path":"/v1/data/research-grants","group":"Health & medicine","auth":"api_key","description":"NIH/HHS-funded research projects — abstracts, funding, PIs (NIH RePORTER)."},{"method":"GET","path":"/v1/data/pubmed","group":"Health & medicine","auth":"api_key","description":"PubMed biomedical literature search (NCBI eutils)."},{"method":"GET","path":"/v1/data/health-providers","group":"Health & medicine","auth":"api_key","description":"US healthcare providers by NPI, taxonomy, address (NPPES)."},{"method":"GET","path":"/v1/data/health-insurance-plans","comingSoon":true,"group":"Health & medicine","auth":"api_key","description":"Marketplace health-insurance plan data (Healthcare.gov). (Coming soon — returns 501; blocked on a finicky upstream POST API, not just a missing key.)"},{"method":"GET","path":"/v1/data/health-topics","group":"Health & medicine","auth":"api_key","description":"Evidence-based preventive-health recommendations by age/sex (health.gov MyHealthfinder, public domain)."},{"method":"GET","path":"/v1/data/dailymed","group":"Health & medicine","auth":"api_key","description":"Full FDA drug labels (Structured Product Labeling) by name, NDC, or set id (NLM DailyMed, public domain)."},{"method":"GET","path":"/v1/data/cdc","group":"Health & medicine","auth":"api_key","description":"US public-health statistics — chronic disease indicators, disease surveillance (CDC open data)."},{"method":"GET","path":"/v1/data/recalls","group":"Health & medicine","auth":"api_key","description":"Food, drug, and device recalls (openFDA)."},{"method":"GET","path":"/v1/data/space","comingSoon":true,"group":"Science & space","auth":"api_key","description":"NASA datasets — APOD, near-earth objects, Mars rover photos, EPIC imagery. (Coming soon — returns 501; needs a NASA key.)"},{"method":"GET","path":"/v1/data/holidays","group":"Reference & culture","auth":"api_key","description":"Public holidays for 100+ countries (Nager.Date)."},{"method":"GET","path":"/v1/data/wiki","group":"Reference & culture","auth":"api_key","description":"Wikipedia article summaries and search (Wikimedia REST)."},{"method":"GET","path":"/v1/data/books","group":"Reference & culture","auth":"api_key","description":"Book metadata, authors, and cover images by ISBN/title (Open Library)."},{"method":"GET","path":"/v1/data/tv","group":"Reference & culture","auth":"api_key","description":"TV show metadata and search (TVMaze)."},{"method":"GET","path":"/v1/data/music","group":"Reference & culture","auth":"api_key","description":"Artist, release, and recording metadata (MusicBrainz)."},{"method":"GET","path":"/v1/data/breweries","group":"Reference & culture","auth":"api_key","description":"Brewery directory search by city/name/type/postal code (Open Brewery DB)."},{"method":"GET","path":"/v1/data/dictionary","group":"Reference & culture","auth":"api_key","description":"Word definitions, phonetics, and parts of speech (Free Dictionary)."},{"method":"GET","path":"/v1/data/words","group":"Reference & culture","auth":"api_key","description":"Find words by meaning, rhyme, sound-alike, or topic (Datamuse)."},{"method":"GET","path":"/v1/data/parks","comingSoon":true,"group":"Reference & culture","auth":"api_key","description":"US National Park info, alerts, events, fees (NPS). (Coming soon — returns 501; needs a data.gov key.)"}],"cliCommands":["data weather --lat <n> --lng <n>","data forex --base USD","data world-bank --country US","data food-products --barcode <upc>","data drugs --name ibuprofen","data clinical-trials --cond \"lung cancer\"","data research-grants --text crispr","data wiki --title <article>"],"enabled":false},{"id":"geo","name":"Geo","tagline":"Geocoding, boundaries, places, routing, timezones, IP & country lookups — one location API.","description":"Location primitives over OpenStreetMap and other open data: forward/reverse geocoding, a global place gazetteer, administrative boundary polygons (continents → countries → states → counties) with point-in-region lookups, distance & routing, timezone and elevation lookups, IP geolocation, and country/postal metadata.","pitch":"Geocoding, reverse geocoding, distance and routing, timezones, elevation, IP lookup. OpenStreetMap does the hard part. We added a cache and a bill.","basePath":"/v1/geo","category":"ingest","status":"available","alwaysOn":false,"howItWorks":"Live — ten location primitives over keyless open data, each normalized to one shape. Forward/reverse geocoding and region lookup proxy Photon (Komoot), an OpenStreetMap-backed geocoder that permits hosted use (ODbL); places proxies OSM Overpass; distance/routing proxy the public OSRM server (car profile today); timezone and elevation come from Open-Meteo; IP geolocation uses ipwho.is; country metadata comes from the World Bank (flags via flagcdn); postal lookup uses Zippopotam. regions resolves continent → country → state → county straight from the reverse geocode (no hosted polygons), so the answer's source is always \"osm\". Inputs are coordinates and short strings — never caller-supplied URLs — so there's no request-forgery surface. Two endpoints are still coming: the GeoNames gazetteer (needs an account) and boundary polygons (the multi-resolution GeoJSON tiers need a self-hosted dataset, not a live proxy that would return multi-megabyte blobs). Parcels (/v1/geo/parcel) is a different kind of lookup: property boundary + attributes (APN, owner, situs address, acreage, county) for a US address or lat/lon, served from a self-hosted, per-state FlatGeobuf index built by a local ETL from free statewide bulk downloads (WI/MN/FL first) — not a live proxy. Every 200 carries a reference-grade disclaimer: this is public county/state GIS data, not a legal survey. GET /v1/geo/parcel/coverage lists exactly which states are loaded, each source's version/license/publish date, and its bbox — check it (or the license per state) before relying on parcel data for a jurisdiction, since attribution requirements vary by source. Upstreams + licensing: _docs/cheapass-upstreams-reference.md.","getStarted":["curl \"$API/v1/geo/geocode?q=Eiffel+Tower\" -H \"Authorization: Bearer $KEY\" — address → coordinates.","curl \"$API/v1/geo/regions?lat=34.05&lng=-118.24\" -H \"Authorization: Bearer $KEY\" — which continent/country/state/county a point is in.","curl \"$API/v1/geo/distance?from=48.86,2.29&to=48.86,2.34\" -H \"Authorization: Bearer $KEY\" — driving distance + duration.","curl \"$API/v1/geo/parcel?address=123+Main+St,+Madison,+WI\" -H \"Authorization: Bearer $KEY\" — parcel boundary + attributes for an address.","curl \"$API/v1/geo/parcel/coverage\" -H \"Authorization: Bearer $KEY\" — which states are loaded, plus source/license/attribution per state."],"useCases":["Geocode addresses and reverse-geocode coordinates without a Google Maps bill.","Resolve which continent, country, state, and county a coordinate falls in — or a visitor's country/city/timezone from their IP.","Get driving distance + duration between two points, the timezone or elevation at a location, or postal-code coordinates for 60+ countries.","Look up a property's parcel boundary, owner-of-record, acreage, and county for an address or coordinate — check /v1/geo/parcel/coverage first for which states are loaded and their attribution terms."],"endpoints":[{"method":"GET","path":"/v1/geo/geocode","group":"Geocoding","auth":"api_key","description":"Forward geocode an address/place name to coordinates (Photon/OSM)."},{"method":"GET","path":"/v1/geo/reverse-geocode","group":"Geocoding","auth":"api_key","description":"Reverse geocode coordinates to an address (Photon/OSM)."},{"method":"GET","path":"/v1/geo/places","group":"Geocoding","auth":"api_key","description":"Find places/POIs near a point by tag (OSM Overpass)."},{"method":"GET","path":"/v1/geo/gazetteer","comingSoon":true,"group":"Geocoding","auth":"api_key","description":"Search the global place gazetteer — name → coordinates, country, admin1/admin2, population, feature class, elevation, timezone (GeoNames; CC-BY, attributed). (Coming soon — returns 501 until a GeoNames account is wired; until then, use geocode.)"},{"method":"GET","path":"/v1/geo/boundaries","comingSoon":true,"group":"Boundaries & regions","auth":"api_key","description":"Boundary polygons as GeoJSON — continents, countries, states/provinces, and counties/districts, at selectable resolution (Natural Earth, public domain + geoBoundaries gbOpen, CC-BY). (Coming soon — returns 501. The multi-resolution polygon tiers need a self-hosted dataset; for point-in-region answers today, use /v1/geo/regions.)"},{"method":"GET","path":"/v1/geo/regions","group":"Boundaries & regions","auth":"api_key","description":"Point-in-region lookup — given coordinates, return the continent, country, state/province, and county that contain them. Resolved from OpenStreetMap's reverse geocode (Photon), so it needs no hosted polygons; source is always \"osm\"."},{"method":"GET","path":"/v1/geo/distance","group":"Routing & distance","auth":"api_key","description":"Driving distance and route between two coordinates (public OSRM server; car profile). Pass geometry=true for the route LineString as GeoJSON."},{"method":"GET","path":"/v1/geo/timezone","group":"Lookups","auth":"api_key","billing":"free_tier_only","description":"IANA timezone for coordinates (Open-Meteo)."},{"method":"GET","path":"/v1/geo/elevation","group":"Lookups","auth":"api_key","billing":"free_tier_only","description":"Elevation in meters for coordinates (Open-Meteo)."},{"method":"GET","path":"/v1/geo/ip","group":"Lookups","auth":"api_key","description":"Geolocate an IP address — country, city, coordinates, ASN, org (ipwho.is)."},{"method":"GET","path":"/v1/geo/country-info","group":"Lookups","auth":"api_key","description":"Country metadata — capital, region, income level, coordinates, flag (World Bank; flag via flagcdn). Pass an ISO2 or ISO3 country code."},{"method":"GET","path":"/v1/geo/postal","group":"Lookups","auth":"api_key","description":"Postal/ZIP code lookup for 60+ countries (Zippopotam)."},{"method":"GET","path":"/v1/geo/parcel","group":"Parcels","auth":"api_key","description":"Property parcel boundary + attributes (APN, owner, situs address, acreage, county) for a US address or lat/lon, served from self-hosted per-state FlatGeobuf files (built from free statewide bulk downloads — not a live upstream proxy). The smallest-area containing parcel is returned as `parcel`; any other overlapping matches come back as `alternates`. Every 200 carries a reference-grade disclaimer — this is public county/state GIS data, not a legal survey. Errors: 404 `parcel_not_covered` when the resolved state isn't loaded yet (includes `loadedStates`); 404 `parcel_not_found` for a gap inside an otherwise-loaded state; 422 `address_not_found` when the address can't be geocoded. Check /v1/geo/parcel/coverage for which states are loaded, plus each source's license/attribution.","example":"{\n  \"parcel\": {\n    \"apn\": \"0710-123-4567\",\n    \"owner\": \"PUBLIC JOHN Q\",\n    \"situsAddress\": \"123 MAIN ST\",\n    \"acreage\": 0.25,\n    \"county\": \"DANE\",\n    \"state\": \"WI\",\n    \"sourceId\": \"wi_v12\",\n    \"sourceVersion\": \"WI-V12-2026\",\n    \"boundary\": { \"type\": \"MultiPolygon\", \"coordinates\": [[[[-89.401, 43.073], [-89.4008, 43.073], [-89.4008, 43.0732], [-89.401, 43.0732], [-89.401, 43.073]]]] }\n  },\n  \"alternates\": [],\n  \"geocode\": { \"lat\": 43.073, \"lon\": -89.401, \"quality\": \"exact\" },\n  \"disclaimer\": \"Reference-grade parcel geometry from public county/state GIS sources — not a legal survey.\"\n}"},{"method":"GET","path":"/v1/geo/parcel/coverage","group":"Parcels","auth":"api_key","description":"Which states have parcel data loaded, and each one's source id, version, license, feature count, and publish date — the attribution surface some source licenses require (read the `license` per state before redistributing). Also useful to call before /v1/geo/parcel to check a jurisdiction is loaded.","example":"{\n  \"generatedAt\": \"2026-07-04T20:30:00.000Z\",\n  \"states\": {\n    \"WI\": { \"sourceId\": \"wi_v12\", \"version\": \"WI-V12-2026\", \"license\": \"Public domain\", \"count\": 1823456, \"publishedAt\": \"2026-07-04T20:30:00.000Z\", \"bbox\": [-92.9, 42.4, -86.2, 47.1] }\n  }\n}"}],"cliCommands":["geo geocode <query>","geo reverse --lat <n> --lng <n>","geo regions --lat <n> --lng <n>","geo distance --from <lat,lng> --to <lat,lng>","geo timezone --lat <n> --lng <n>","geo elevation --lat <n> --lng <n>","geo ip <address>","geo country <code>","geo postal --country US --code 02139","geo parcel --address <address> | --lat <n> --lng <n>","geo parcel coverage"],"enabled":false},{"id":"utils","name":"Utils","tagline":"Small, fast developer utilities — QR, favicon, avatars, hashing, DNS, validation, and more.","description":"A grab-bag of cheap, mostly-local developer utilities: generate QR codes and avatars, fetch favicons, validate emails, look up DNS/WHOIS, hash and encode strings, render markdown or charts, and filter profanity. Most run locally for near-zero cost.","pitch":"QR codes, avatars, hashes, DNS, WHOIS, email validation, JWT decode, markdown render. The things you'd paste from Stack Overflow, now with a stable shape and an uptime page.","basePath":"/v1/utils","category":"build","status":"available","alwaysOn":false,"howItWorks":"Live. Most utilities are pure computation that never leaves the API: hash (SHA-1/256/384/512 via Web Crypto), base64, jwt-decode (decode only — no signature check), slug, markdown-render (escape-first safe subset), and profanity. The rest are thin proxies to keyless upstreams: QR via goqr.me, avatars via DiceBear, charts via QuickChart, DNS over DoH (Cloudflare), WHOIS over RDAP, favicons via Google's S2 service, and email-validate (format + live MX lookup + disposable-domain check). Image endpoints (qr/avatar/chart/favicon) return raw bytes you can drop in an <img src>; everything else returns JSON. The one exception is shortlink, still in progress — it 501s until the click-tracking store lands.","getStarted":["Live — enable it, then call any utility (only shortlink is still coming).","curl \"$API/v1/utils/qr?data=https://cheapass.dev&format=svg\" -H \"Authorization: Bearer $KEY\" — make a QR code."],"useCases":["Generate QR codes, avatars, or charts as images from a backend.","Validate emails or look up DNS/WHOIS in a signup or moderation flow.","Hash, slugify, base64, or decode a JWT without pulling in libraries."],"endpoints":[{"method":"GET","path":"/v1/utils/qr","group":"Generators","auth":"api_key","description":"Generate a QR code as SVG or PNG."},{"method":"GET","path":"/v1/utils/avatar","group":"Generators","auth":"api_key","description":"Generate a deterministic avatar from a seed (DiceBear)."},{"method":"POST","path":"/v1/utils/chart","group":"Generators","auth":"api_key","description":"Render a Chart.js spec to a PNG/SVG image (self-hosted QuickChart)."},{"method":"POST","path":"/v1/utils/shortlink","comingSoon":true,"group":"Generators","auth":"api_key","description":"Create a short link for a destination URL. (Coming soon — returns 501 until the click-tracking store lands; the dedicated links/* product will own this.)"},{"method":"GET","path":"/v1/utils/email-validate","group":"Validation & lookup","auth":"api_key","description":"Validate an email — format, MX record, and disposable-domain check."},{"method":"GET","path":"/v1/utils/whois","group":"Validation & lookup","auth":"api_key","description":"Domain registration data via RDAP."},{"method":"GET","path":"/v1/utils/dns","group":"Validation & lookup","auth":"api_key","description":"DNS record lookup (A/AAAA/MX/TXT/NS/CNAME) over DoH."},{"method":"GET","path":"/v1/utils/favicon","group":"Validation & lookup","auth":"api_key","description":"Resolve and return a site's favicon as an image (Google S2). ?size= 16–256."},{"method":"POST","path":"/v1/utils/hash","group":"Encoding & crypto","auth":"api_key","description":"Hash a string with the SHA family (sha1, sha256, sha384, sha512) via Web Crypto."},{"method":"POST","path":"/v1/utils/base64","group":"Encoding & crypto","auth":"api_key","description":"Base64 encode or decode."},{"method":"POST","path":"/v1/utils/jwt-decode","group":"Encoding & crypto","auth":"api_key","description":"Decode a JWT's header and payload (no signature verification)."},{"method":"POST","path":"/v1/utils/slug","group":"Text","auth":"api_key","description":"Slugify a string (URL-safe)."},{"method":"POST","path":"/v1/utils/markdown-render","group":"Text","auth":"api_key","description":"Render markdown to sanitized HTML."},{"method":"POST","path":"/v1/utils/profanity","group":"Text","auth":"api_key","description":"Detect and optionally mask profanity in text."}],"cliCommands":["utils hash <text> --algorithm sha256","utils base64 <text> [--decode]","utils slug <text>","utils dns <name> --type A","utils email-validate <email>","utils whois <domain>","utils qr <data> --out qr.png","utils avatar <seed> --out avatar.svg"],"enabled":false},{"id":"events","name":"Events","tagline":"Real-time structured event streams — disasters, space, aviation, internet health.","description":"Polled, normalized real-time event streams from authoritative public sources: weather alerts, hurricanes, wildfires, volcanoes, earthquakes, tsunamis, space weather, launches, ISS position, internet health, and more. Structured data (not articles) — subscribe by webhook or poll.","pitch":"Real-time event streams — weather alerts, earthquakes, wildfires, space weather, rocket launches, the ISS passing overhead. Poll it or take a webhook. The apocalypse, normalized as JSON.","basePath":"/v1/events","category":"ingest","status":"coming_soon","alwaysOn":false,"howItWorks":"Status: planned — endpoints return 501 until shipped. Each stream polls its upstream on a cadence appropriate to the source (per the shared-upstream principle — fetched once globally, served from cache), normalizes events to a common { id, type, severity, title, occurredAt, geometry, source, raw } shape, and can push new events to a registered webhook. Free, public, commercial-OK upstreams only (the reference's skip-list providers are intentionally excluded).","getStarted":["(Not yet available — these endpoints return 501 not_implemented today.)","When shipped: cheapass products enable events.","curl \"$API/v1/events/earthquakes?window=day&minMagnitude=4\" -H \"Authorization: Bearer $KEY\"."],"useCases":["Power a disaster/alerting dashboard from one normalized event API.","Trigger workflows on new events (webhook) instead of polling many agencies.","Map real-time hazards, launches, or internet outages."],"endpoints":[{"method":"GET","path":"/v1/events/weather-alerts","group":"Natural hazards","auth":"api_key","description":"Active NWS weather alerts by area (US National Weather Service)."},{"method":"GET","path":"/v1/events/hurricanes","group":"Natural hazards","auth":"api_key","description":"Active tropical cyclones and advisories (NOAA NHC)."},{"method":"GET","path":"/v1/events/wildfires","group":"Natural hazards","auth":"api_key","description":"Satellite-detected active fire hotspots (NASA FIRMS)."},{"method":"GET","path":"/v1/events/volcanoes","group":"Natural hazards","auth":"api_key","description":"Current volcanic eruptions (Smithsonian GVP)."},{"method":"GET","path":"/v1/events/earthquakes","group":"Natural hazards","auth":"api_key","description":"Real-time earthquakes over a time window (USGS feeds)."},{"method":"GET","path":"/v1/events/tsunamis","group":"Natural hazards","auth":"api_key","description":"Tsunami warnings and advisories (NOAA PTWC)."},{"method":"GET","path":"/v1/events/natural-events","group":"Natural hazards","auth":"api_key","description":"Combined open natural-event stream — fires, storms, volcanoes, floods (NASA EONET)."},{"method":"GET","path":"/v1/events/space-weather","group":"Space & sky","auth":"api_key","description":"Solar flares, geomagnetic storms, Kp index, aurora forecasts (NOAA SWPC)."},{"method":"GET","path":"/v1/events/iss-position","group":"Space & sky","auth":"api_key","description":"Current International Space Station position and velocity (Where The ISS At)."},{"method":"GET","path":"/v1/events/launches","group":"Space & sky","auth":"api_key","description":"Upcoming and recent rocket launches (Launch Library 2)."},{"method":"GET","path":"/v1/events/satellite-passes","group":"Space & sky","auth":"api_key","description":"Visible satellite passes for a location (CelesTrak TLE + SGP4)."},{"method":"GET","path":"/v1/events/asteroids","group":"Space & sky","auth":"api_key","description":"Near-earth objects and close approaches (NASA NeoWs)."},{"method":"GET","path":"/v1/events/conflict","group":"Geopolitics & internet","auth":"api_key","description":"Geolocated world-news events with actor/theme tagging (GDELT)."},{"method":"GET","path":"/v1/events/internet-health","group":"Geopolitics & internet","auth":"api_key","description":"Internet traffic, BGP, DDoS, and country outage signals (Cloudflare Radar)."},{"method":"GET","path":"/v1/events/bgp","group":"Geopolitics & internet","auth":"api_key","description":"BGP routing and ASN/prefix info (RIPEstat / BGPview)."},{"method":"GET","path":"/v1/events/airport-status","group":"Aviation & maritime","auth":"api_key","description":"US airport delays, ground stops, and closures (FAA)."},{"method":"GET","path":"/v1/events/ships","group":"Aviation & maritime","auth":"api_key","description":"AIS vessel positions in US waters (MarineCadastre)."},{"method":"POST","path":"/v1/events/subscribe","group":"Delivery","auth":"api_key","description":"Register a webhook to receive new events from a stream as they appear."}],"cliCommands":["events earthquakes --window day --min-magnitude 4","events weather-alerts --area MA","events launches --upcoming","events subscribe <stream> --webhook <url>"],"enabled":false},{"id":"transit","name":"Transit","tagline":"Public-transit agencies, routes, stops, real-time positions, and trip planning.","description":"Public-transit data built on OpenTripPlanner + GTFS feeds: list agencies/routes/stops, get real-time vehicle positions and service alerts, and plan multimodal trips between two points.","pitch":"Agencies, routes, stops, live vehicle positions, and trip planning. OpenTripPlanner under the hood. We run the Java so you don't have to.","basePath":"/v1/transit","category":"ingest","status":"coming_soon","alwaysOn":false,"howItWorks":"Status: planned — endpoints return 501 until shipped. We self-host OpenTripPlanner (Apache 2.0), load GTFS feeds for covered agencies (from the Mobility Database / Transitland), and wrap OTP's API with our auth + metering. Real-time positions and alerts come from agencies' GTFS-RT feeds. Coverage is per-agency; feed licenses vary.","getStarted":["(Not yet available — these endpoints return 501 not_implemented today.)","When shipped: cheapass products enable transit.","curl \"$API/v1/transit/plan?from=42.36,-71.06&to=42.35,-71.05\" -H \"Authorization: Bearer $KEY\"."],"useCases":["Show nearby stops and real-time arrivals in an app.","Plan a multimodal transit trip between two coordinates.","Surface service alerts for routes a user follows."],"endpoints":[{"method":"GET","path":"/v1/transit/agencies","auth":"api_key","description":"List covered transit agencies."},{"method":"GET","path":"/v1/transit/routes","auth":"api_key","description":"Routes for an agency."},{"method":"GET","path":"/v1/transit/stops","auth":"api_key","description":"Stops near a point or on a route."},{"method":"GET","path":"/v1/transit/realtime","auth":"api_key","description":"Real-time vehicle positions, trip updates, and service alerts (GTFS-RT)."},{"method":"GET","path":"/v1/transit/plan","auth":"api_key","description":"Plan a multimodal trip between two points (OpenTripPlanner)."}],"cliCommands":["transit agencies","transit stops --lat <n> --lng <n>","transit realtime --route <id>","transit plan --from <lat,lng> --to <lat,lng>"],"enabled":false},{"id":"links","name":"Links","tagline":"Create branded short links and track clicks.","description":"Headless branded short links. Mint a short slug for a destination URL, serve the redirect, and count clicks. Planned: custom domains, expiry, QR generation, analytics export.","pitch":"Short links that count clicks. The entire “we built a URL shortener” product, hosted, so you can delete that folder.","basePath":"/v1/links","category":"publish","status":"coming_soon","alwaysOn":false,"howItWorks":"(Planned.) POST /v1/links/shorten with a destination URL (and optional custom slug / domain) returns a short link; GET /v1/links lists your links with click counts. Not yet implemented — responses today are 501 not_implemented.","getStarted":["(Not yet available — this product returns 501 not_implemented today.)","When shipped: cheapass products enable links, then POST /v1/links/shorten."],"useCases":["Generate trackable short links from a marketing automation pipeline.","Branded shareable URLs with click analytics behind one API."],"endpoints":[{"method":"POST","path":"/v1/links/shorten","description":"Create a short link. (Coming soon — 501 today.)","auth":"api_key"},{"method":"GET","path":"/v1/links","description":"List your short links with click counts. (Coming soon — 501 today.)","auth":"api_key"}],"cliCommands":["links — (no commands yet; product returns 501)"],"enabled":false},{"id":"og","name":"OG Images","tagline":"Social-card images from a GET request — a URL you can put straight in an og:image tag.","description":"Open Graph / social-card generation. Pass a title, subtitle, and theme (or a saved template) and get back a 1200×630 PNG for og:image and twitter:image — the request URL itself is the image, so it goes directly in the meta tag. Cards are layer stacks: background, hero image (with filters), text, and pinned badges compose bottom-up in one request. Planned: saved templates and brand kits (logo, colors, fonts). Processing of existing images (resize, convert, watermark) is not duplicated here — cv/* already does it, live today.","pitch":"og:image as a GET request — title in, 1200×630 PNG out, cached. Cards are layer stacks — gradient backdrop, hero image, grayscale filter, text, a logo box pinned to a corner. Satori does the hard part (Vercel open-sourced it); we host it and hand you a URL your meta tags can use as-is.","basePath":"/v1/og","category":"build","status":"available","alwaysOn":false,"howItWorks":"A card is a stack of layers rendered bottom-up: background (solid color or CSS gradient), image (any public URL — object-fit, position, filters like grayscale), text (content, size, weight, letter-spacing; give it x/y to position absolutely, omit them to flow in a centered column), and badge (a bordered box pinned to a corner — your logo over any art). Rendering happens on a Cloudflare Worker running satori (layout → SVG) and resvg (SVG → PNG), with JetBrains Mono bundled. GET /v1/og/image?title=…&subtitle=…&theme=light|dark is the simple form — it fills the default template and, because meta tags can't send headers, this endpoint (alone) also accepts your API key as ?key=. POST /v1/og/render takes the full { layers: [...] } stack. Every render is cached content-addressed by its exact spec — the same URL always serves the same bytes, one render serves every tenant who asks, and crawlers can treat it as a static image. Saved templates (POST /v1/og/templates, filled per-request as ?template=<id>) are planned and return 501 today. Processing of existing images stays in cv/* (live).","getStarted":["cheapass login — authenticate the CLI.","cheapass keys create — mint an API key.","curl \"$API/v1/og/image?title=Ship%20day&theme=dark&key=$KEY\" -o card.png — render a card.","Put that same URL in <meta property=\"og:image\" content=\"…\"> — the URL is the image.","curl -X POST $API/v1/og/render -H \"Authorization: Bearer $KEY\" -d '{\"layers\":[{\"type\":\"background\",\"color\":\"#131311\"},{\"type\":\"text\",\"content\":\"Hello\",\"size\":72,\"color\":\"#F8F8F1\"}]}' -o card.png — full layer control."],"useCases":["Per-page social cards for a blog or docs site without running a headless browser in your build.","Dynamic share images — user names, scores, stats — rendered on request and cached.","News-site article cards: hero image layer, grayscale filter, brand box pinned bottom-right — one layer stack.","One saved template for the whole site: pass only the title, the brand kit fills in the rest."],"endpoints":[{"method":"GET","path":"/v1/og/image","auth":"api_key","description":"Render a 1200×630 social-card PNG from query params (title, subtitle, theme). Accepts ?key= so the URL works inside an og:image meta tag. Cached by exact params."},{"method":"POST","path":"/v1/og/render","auth":"api_key","description":"Render a card from a full { layers: [...] } stack — background, images, text, badges. Cached by the exact spec."},{"method":"POST","path":"/v1/og/templates","auth":"api_key","comingSoon":true,"description":"Save a reusable card template — a layer stack with named text slots. (Coming soon — 501 today.)"},{"method":"GET","path":"/v1/og/templates","auth":"api_key","comingSoon":true,"description":"List your saved templates. (Coming soon — 501 today.)"}],"cliCommands":["og render --title <text> [--subtitle <text>] [--theme light|dark] --out card.png","og render --layers layers.json --out card.png"],"enabled":false},{"id":"trends","name":"Trends","tagline":"News-anchored cultural momentum. Track how much the world is paying attention to a term over time, and what's rising now.","description":"Given a watchlist of terms, Trends tracks each term's momentum over time by blending free, official signals (GDELT news-volume, Wikipedia pageviews, and RSS rising-terms). Surfaces both interest-over-time (for graphs) and currently-rising terms (for discovery). A separate Topics pipeline derives categorized trending topics (themes, entities, products, tags — cultural/consumer/political/tech/business) directly from content, filterable by the sources they derive from.","pitch":"Watch a list of terms and get back cultural momentum: GDELT news volume, Wikipedia pageviews, and rising terms from our own feed corpus, blended into interest-over-time and what's-rising-now. Free official signals, one shape.","basePath":"/v1/trends","category":"ingest","status":"available","alwaysOn":false,"howItWorks":"An hourly job polls each watched term across GDELT and Wikipedia, normalizes each source to 0–100, and rolls them into a weighted composite with a velocity (rising/falling). A separate discovery job surfaces newly-rising terms from Wikipedia's top pages and RSS term-frequency spikes, auto-adding the strongest to the watchlist. The Topics pipeline works content-first: an hourly LLM pass extracts topics from captured feed items, mentions are bucketed per (topic, source, hour), and momentum is precomputed per source and overall — so ?sources= filters recompute scores from that subset's data rather than filtering rows.","getStarted":["cheapass login","cheapass trends watch 'taylor swift' 'ai regulation'","cheapass trends top"],"useCases":["Track how much attention a news topic is getting over time.","Discover what's rising right now across news + Wikipedia.","Drive editorial/content decisions from real-world momentum.","Pull categorized trend lists (cultural / consumer / political) to seed articles or campaigns."],"endpoints":[{"method":"POST","path":"/v1/trends/watch","group":"Watchlist","description":"Register terms to track. Idempotent on normalized term.","auth":"api_key","example":"{ \"inserted\": 2, \"existing\": 0 }","billing":"billable"},{"method":"GET","path":"/v1/trends/top","group":"Reading","description":"Top movers by composite momentum for a window (24h|7d).","auth":"api_key","example":"[{ \"term\": \"taylor swift\", \"composite\": 87.4, \"velocity\": 12.1, \"rank\": 1 }]","billing":"billable"},{"method":"GET","path":"/v1/trends/term","group":"Reading","description":"Composite + per-source time series for one term.","auth":"api_key","example":"{ \"term\": \"taylor swift\", \"composite\": 87.4, \"series\": [] }","billing":"billable"},{"method":"GET","path":"/v1/trends/rising","group":"Discovery","description":"Currently-rising terms (not necessarily on the watchlist).","auth":"api_key","example":"[{ \"term\": \"quokka\", \"source\": \"rss\", \"approxTraffic\": 12 }]","billing":"billable"},{"method":"GET","path":"/v1/trends/topics","group":"Topics","description":"Ranked content-derived topics. Filter by category, kind, window, and the sources trends derive from (?sources= recomputes scores from that subset).","auth":"api_key","example":"[{ \"topic\": \"airline seat fees\", \"kind\": \"theme\", \"category\": \"consumer\", \"composite\": 87.2, \"velocity\": 12.4, \"rank\": 1, \"sources\": [{ \"source\": \"rss\", \"composite\": 87.2, \"mentions\": 14 }] }]","billing":"billable"},{"method":"GET","path":"/v1/trends/topics/detail","group":"Topics","description":"One topic (canonical name or alias): metadata, per-source momentum across windows, hourly mention series.","auth":"api_key","example":"{ \"topic\": \"airline seat fees\", \"kind\": \"theme\", \"category\": \"consumer\", \"momentum\": [], \"series\": [] }","billing":"billable"}],"cliCommands":["trends watch <term...>","trends top","trends term <term>","trends rising"],"enabled":false},{"id":"sports","name":"Sports","tagline":"Scores for 40+ leagues from upstream APIs we no longer name. Two tiers: Sports Scores ($10/mo, a fresh snapshot every ~10s) and Live Sports Scores ($100/mo, sub-second push over WebSocket). Paid plan required.","description":"Scoreboards, box scores, play-by-play, standings, and teams for 40+ leagues — the US majors plus ~30 soccer leagues and cups worldwide — normalized to one envelope. Sourced from three public, undocumented sports APIs that carry no SLA and, as of this paragraph, no names. That's the product: we poll each league once on a live-aware cadence (~10 seconds during games, hours when nothing's on), validate every payload so upstream schema drift fails loud instead of serving garbage, and give you a stable contract over upstreams that never offered one. Sports headlines ride along from the shared news catalog (Yahoo Sports, BBC Sport, ESPN) as headline + summary + link with mandatory attribution. Sports comes in two tiers, both stacking on a paid plan (Junior or Senior Dev) and included in neither by default: Sports Scores ($10/mo) hands your key a fresh scoreboard snapshot about every 10 seconds; Live Sports Scores ($100/mo) streams every score change over a WebSocket the moment it arrives — sub-second behind the source, with an automatic ~10s poller fallback so the stream never goes dark. Same data either way; the price buys how often you're allowed a new number.","pitch":"Scores for 40+ leagues — the US majors plus ~30 soccer leagues worldwide — normalized to one envelope. Base tier refreshes every ~10s; the Live tier pushes updates sub-second over WebSocket. We poll unnamed upstreams and absorb the risk of building on endpoints nobody promised us.","basePath":"/v1/sports","category":"ingest","addOnId":"sports_live","highlights":[{"label":"US majors","items":["MLB","NHL","NFL","NBA","WNBA","NCAAF","NCAAB","CFL","G League"]},{"label":"Soccer","items":["EPL","LaLiga","Serie A","Bundesliga","Ligue 1","MLS","Liga MX","Eredivisie","Primeira","Brasileirão","Saudi Pro League","J.League","UCL","Europa","Libertadores","World Cup","+ more"]},{"label":"Upstreams","items":["Three public sports APIs","Names withheld — that's the moat"]},{"label":"News sources","items":["Yahoo Sports","BBC Sport","ESPN"]}],"status":"available","alwaysOn":false,"howItWorks":"A background poller watches every league with a cadence tiered by game state: 10 seconds while games are live, every minute in the half hour before first pitch/kickoff/puck drop, every 5 minutes on a game day, hourly on an off day. One upstream poll serves every customer — a thousand of you polling scoreboard/live costs the upstream one request, which is both the economics and the etiquette. Scoreboards are fully normalized to one envelope (league, gameId, status, period, clock, home/away, score) regardless of upstream. Box scores and play-by-play fetch lazily — only games somebody actually asks about — with TTLs tiered the same way, and finals cached forever because a finished game has stopped changing. Every payload passes schema validation before we store it; when an upstream changes a shape (they will), the league's status row flips to errored on our public status page instead of you finding out via NaN. Each league also keeps a truncated raw upstream snapshot so we can diff and patch fast. Honesty section: these upstreams are public, unauthenticated, entirely undocumented web-app APIs. We used to name them right here; we stopped once we worked out that the names were the only thing standing between this add-on and $0/mo. Nobody licensed us to resell anything, so the price buys the cache, the normalization, the fallbacks, and a byline-free bill of materials. The two tiers split on freshness, not data: Sports Scores ($10/mo) serves your key a new snapshot about every 10 seconds; Live Sports Scores ($100/mo) lifts that throttle and streams scores as fast as they land (average update a couple seconds). If an upstream bans us, the status page will say so plainly.","getStarted":["cheapass login","cheapass products enable sports","cheapass sports leagues — see the 15 tracked leagues.","cheapass sports scores mlb — today's scoreboard.","cheapass sports live worldcup — only in-progress matches, live-ish (~10s).","curl \"$API/v1/sports/nba/scoreboard?date=2026-06-19\" -H \"Authorization: Bearer $KEY\" — any past date, cached forever once fetched.","cheapass sports news nfl --sources yahoo,bbc — headlines with attribution."],"useCases":["Put live-ish (~10s) scores in an app without a five-figure sports-data contract.","Poll scoreboard/live every few seconds from thousands of clients while the upstream sees one request per cadence window.","Pull final box scores and play-by-play for analysis — finals never expire from the cache.","Show sports headlines with proper source attribution next to the scores."],"endpoints":[{"method":"GET","path":"/v1/sports/leagues","group":"Reference","description":"Every tracked league: slug, sport, upstream source, live-game count.","auth":"api_key","example":"[{ \"slug\": \"mlb\", \"name\": \"Major League Baseball\", \"sport\": \"baseball\", \"source\": \"mlb\", \"liveGames\": 6 }]","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/scoreboard","group":"Scores","description":"Scoreboard for a date (default: the current window). Normalized envelope regardless of upstream.","auth":"api_key","example":"{ \"league\": \"mlb\", \"games\": [{ \"gameId\": \"824659\", \"status\": \"final\", \"home\": { \"name\": \"Chicago Cubs\", \"score\": 1 }, \"away\": { \"name\": \"St. Louis Cardinals\", \"score\": 17 } }] }","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/scoreboard/live","group":"Scores","description":"Only in-progress games. Throttled to a new snapshot every ~10s on Sports Scores; streamed as fast as scores arrive on Live Sports Scores.","auth":"api_key","example":"{ \"league\": \"worldcup\", \"games\": [{ \"gameId\": \"733839\", \"status\": \"live\", \"clock\": \"73'\", \"period\": 2 }] }","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/games/:id/boxscore","group":"Game detail","description":"Box score, fetched lazily and cached (live: 30s TTL; finals: forever). Compact, source-flavored shape.","auth":"api_key","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/games/:id/playbyplay","group":"Game detail","description":"Play-by-play / match commentary, one compact row per play. Same lazy caching as box scores.","auth":"api_key","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/standings","group":"Reference","description":"League table / division standings. Cached 6h — standings move slowly, unlike scores.","auth":"api_key","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/teams","group":"Reference","description":"Teams in a league, with logos where the upstream provides them. Cached 24h.","auth":"api_key","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/:league/teams/:id","group":"Reference","description":"One team, by upstream id or abbreviation (mlb/teams/CHC works).","auth":"api_key","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/news","group":"News","description":"Sports headlines across all sources: headline + summary + canonical link + attribution. Never full bodies — that's the sources' explicit ceiling, so it's ours too.","auth":"api_key","example":"{ \"items\": [{ \"title\": \"…\", \"source\": \"bbc\", \"attribution\": \"BBC Sport\", \"sourceUrl\": \"https://www.bbc.co.uk/sport/…\" }] }","billing":"free_tier_only"},{"method":"GET","path":"/v1/sports/news/:league","group":"News","description":"Headlines filtered to one league's feeds (mlb, nfl, nba, nhl, soccer, college).","auth":"api_key","billing":"free_tier_only"}],"cliCommands":["sports leagues","sports scores <league> [--date YYYY-MM-DD]","sports live <league>","sports standings <league>","sports teams <league>","sports news [league] [--sources yahoo,bbc,foxsports,espn]"],"enabled":false},{"id":"cv","name":"CV","tagline":"Computer vision and image processing over HTTP. OpenCV and libvips do the hard part; we run them and forward the bill.","description":"One consistent op grammar over two engines: native OpenCV (detection, edges, geometry, comparison) and sharp/libvips (resize, encode, adjust). Every op is GET /v1/cv/<op>?src=<url> with flat, documented params — image in, image or JSON out. Deliberately not all 2,500 OpenCV symbols: anything stateful (video tracking, calibration, training) can't live behind a stateless HTTP call, and we'd rather list what works than sell what doesn't.","pitch":"Computer vision and image processing: fifty-odd ops over native OpenCV and libvips, one URL-in shape. Detect a document's edges, deskew a receipt, score blurriness, diff two frames for motion, resize to AVIF. Deliberately not all 2,500 OpenCV functions — the stateful ones can't live behind HTTP, and we'd rather list what works than sell what doesn't.","basePath":"/v1/cv","category":"build","status":"available","alwaysOn":false,"howItWorks":"One grammar, no exceptions: GET /v1/cv/<op>?src=<image url>&<flat params>. Image ops return bytes (?format=png|jpeg|webp — the sharp ops add avif — and ?quality=); analysis ops return { data }. Two-image ops (compare, template-match, and the Motion group) add ?ref= — for motion, src is the BEFORE frame and ref is the AFTER. Behind the gateway sit three engines you never see: native opencv-python for vision (detection is gradient-based — Canny edges, contours, Hough lines — so it finds a document on textured, unevenly lit paper where background-color scans give up), sharp/libvips for media (resize, encode, adjust), and an ffmpeg container for the Video group (those ops answer 503 video_tier_not_deployed until that container ships — stated in each one's docs, so you find out here and not from a bill). Sources are fetched server-side: public http(s) URLs only, 20 MB per image, 200 MB per video, private addresses refused, redirects re-validated hop by hop. Every error is { code, message, hint } — the hint is written for the agent that will read it. GET /v1/cv/ops is the machine-readable catalog: every op, engine, output kind, and typed param, which is all your agent needs to drive the whole product. Need more than one op but not the whole of OpenCV? POST /v1/cv/pipeline takes { src, steps: [{op, ...params}, …], format?, quality? } and chains named catalog ops in one request — one op per step, image-kind ops only (so every step has bytes to hand the next one), up to 8 steps, with each step's output piped straight into the next across engines (opencv to sharp and back) so your server never touches the intermediate bytes; only the final step's format/quality apply. And when a named op isn't enough, POST /v1/cv/raw is the whole of OpenCV: name any cv2 function (1000+ across cv2 and its submodules), pass arguments through a small codec ({\"$image\":url}, {\"$array\":[…]}, {\"$const\":\"MORPH_RECT\"}, {\"$ref\":\"savedStep\"}), and chain steps (save a result, reference it downstream) so pipelines and class-based APIs like SIFT work in one call. Your agent already knows the cv2 API from docs.opencv.org — the bridge just runs it. Only GUI, disk/camera I/O, and file-model-loading are blocked, because they're meaningless over HTTP; every call runs in an isolated subprocess so a native crash is a 422 for you, not an outage for anyone else.","getStarted":["cheapass login","cheapass cv ops","cheapass cv run blur-score --src https://example.com/photo.jpg","cheapass cv run resize --src https://example.com/photo.jpg --set w=800 --set format=webp --out small.webp","cheapass cv run motion-score --src https://cam.example.com/before.jpg --ref https://cam.example.com/after.jpg","curl -X POST $API/v1/cv/raw -H \"Authorization: Bearer $KEY\" -d '{\"steps\":[{\"fn\":\"Canny\",\"args\":[{\"$image\":\"https://example.com/p.jpg\"},50,150]}]}'"],"useCases":["QA generated images: score sharpness, hash for duplicates, reject bad renders before publishing.","Auto-crop comic strips, scans, and screenshots; deskew and perspective-correct document photos.","Resize, convert, and compress to webp/avif without deploying libvips yourself.","Redact or pixelate regions, decode QR codes, find a logo inside a screenshot.","Motion detection from any camera that can upload two frames: diff them, score them, get region boxes.","Video triage (once the video tier ships): scene cuts, motion timeline, keyframe contact sheets."],"endpoints":[{"method":"GET","path":"/v1/cv/ops","group":"Catalog","description":"The machine-readable op catalog: every op, its engine, output kind, and typed params. Public — this is documentation.","auth":"none"},{"method":"GET","path":"/v1/cv/blur-score","group":"Detect","description":"Sharpness score (variance of Laplacian) with a plain verdict.","auth":"api_key","example":"{ \"score\": 812.44, \"verdict\": \"sharp\" }","billing":"billable"},{"method":"GET","path":"/v1/cv/detect-bounds","group":"Detect","description":"Content bounding box as fractional coordinates, via Canny edges + contours + Hough lines.","auth":"api_key","example":"{ \"found\": true, \"x\": 0.08, \"y\": 0.11, \"w\": 0.84, \"h\": 0.62, \"confidence\": 0.9 }","billing":"billable"},{"method":"GET","path":"/v1/cv/smart-trim","group":"Transform","description":"Crop to detected content with even padding; declines (JSON) when already tight.","auth":"api_key","example":"{ \"trimmed\": false, \"reason\": \"already-tight\" }","billing":"billable"},{"method":"GET","path":"/v1/cv/contours","group":"Detect","description":"External contours: bounding boxes, areas, vertex counts (largest first).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/hough-lines","group":"Detect","description":"Straight line segments (probabilistic Hough), endpoints in fractional coords.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/hough-circles","group":"Detect","description":"Circles (Hough transform): center + radius in fractional coords.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/corners","group":"Detect","description":"Strong corners (Shi–Tomasi) as fractional points.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/histogram","group":"Detect","description":"Per-channel 16-bin intensity histograms (fractions).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/dominant-colors","group":"Detect","description":"K-means dominant colors with population share.","auth":"api_key","example":"{ \"colors\": [{ \"hex\": \"#f3f4f4\", \"share\": 0.55 }] }","billing":"billable"},{"method":"GET","path":"/v1/cv/phash","group":"Detect","description":"64-bit perceptual hash (DCT) as hex — stable under resize/re-encode.","auth":"api_key","example":"{ \"phash\": \"3c1ee3e2c3f83c09\" }","billing":"billable"},{"method":"GET","path":"/v1/cv/compare","group":"Detect","description":"Similarity of src vs ref: pHash Hamming distance + histogram correlation.","auth":"api_key","example":"{ \"phashDistance\": 4, \"histogramCorrelation\": 1.0, \"likelySame\": true }","billing":"billable"},{"method":"GET","path":"/v1/cv/template-match","group":"Detect","description":"Find the ref image inside src (normalized cross-correlation).","auth":"api_key","example":"{ \"found\": true, \"score\": 1.0, \"x\": 0.31, \"y\": 0.33, \"w\": 0.12, \"h\": 0.13 }","billing":"billable"},{"method":"GET","path":"/v1/cv/qr-decode","group":"Detect","description":"Detect + decode QR codes.","auth":"api_key","example":"{ \"codes\": [{ \"value\": \"https://example.com\", \"x\": 0.1, \"y\": 0.1, \"w\": 0.3, \"h\": 0.3 }] }","billing":"billable"},{"method":"GET","path":"/v1/cv/optical-flow","group":"Motion","description":"Dense optical flow (Farnebäck) from src to ref: overall magnitude, dominant direction, and a coarse vector grid.","auth":"api_key","example":"{ \"meanMagnitude\": 0.011, \"maxMagnitude\": 0.187, \"direction\": 114.1, \"grid\": 8, \"vectors\": [] }","billing":"billable"},{"method":"GET","path":"/v1/cv/motion-score","group":"Motion","description":"How much changed between two frames: changed-pixel fraction + bounding boxes of the changed regions.","auth":"api_key","example":"{ \"score\": 0.0985, \"moving\": true, \"regions\": [{ \"x\": 0.24, \"y\": 0.32, \"w\": 0.27, \"h\": 0.36 }] }","billing":"billable"},{"method":"GET","path":"/v1/cv/frame-diff","group":"Motion","description":"The changed-pixels mask between two frames (white on black).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/canny","group":"Edges","description":"Canny edge map (white edges on black).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/sobel","group":"Edges","description":"Sobel gradient magnitude.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/threshold","group":"Tone","description":"Binary threshold (Otsu when no value given).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/adaptive-threshold","group":"Tone","description":"Adaptive Gaussian threshold — survives uneven lighting.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/clahe","group":"Tone","description":"Contrast-limited adaptive histogram equalization.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/equalize","group":"Tone","description":"Global histogram equalization (luminance).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/erode","group":"Morphology","description":"Morphological erosion.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/dilate","group":"Morphology","description":"Morphological dilation.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/morph-open","group":"Morphology","description":"Opening (erode then dilate) — removes specks.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/morph-close","group":"Morphology","description":"Closing (dilate then erode) — fills pinholes.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/denoise","group":"Morphology","description":"Non-local-means color denoising.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/blur-bilateral","group":"Morphology","description":"Edge-preserving bilateral smoothing.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/deskew","group":"Geometry","description":"Detect the dominant text/content angle and rotate it level.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/perspective-correct","group":"Geometry","description":"Find the largest quadrilateral (document/screen) and warp it to a straight-on rectangle.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/pixelate","group":"Geometry","description":"Mosaic pixelation (privacy/effect).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/redact","group":"Geometry","description":"Pixelate one rectangle (fractional coords) — receipts, faces, keys.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/resize","group":"Media","description":"Resize with fit modes; never upscales unless asked.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/convert","group":"Media","description":"Re-encode to png/jpeg/webp/avif at a quality (metadata stripped).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/crop","group":"Media","description":"Crop to a rectangle in fractional coordinates.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/pad","group":"Media","description":"Extend the canvas to an exact aspect ratio on a background color.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/trim","group":"Media","description":"Fast background trim (libvips) — uniform borders only; use smart-trim for textured paper.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/rotate","group":"Media","description":"Rotate by degrees (canvas grows; background fills corners).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/flip","group":"Media","description":"Mirror vertically (flip) and/or horizontally (flop).","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/blur","group":"Media","description":"Gaussian blur.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/sharpen","group":"Media","description":"Unsharp mask.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/grayscale","group":"Media","description":"Convert to grayscale.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/negate","group":"Media","description":"Invert colors.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/tint","group":"Media","description":"Tint via a color while preserving luminance.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/modulate","group":"Media","description":"Adjust brightness / saturation / hue-rotation.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/gamma","group":"Media","description":"Gamma correction.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/normalize","group":"Media","description":"Stretch contrast to the full range.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/composite","group":"Media","description":"Overlay a second image onto the base at a gravity/offset, with optional resize + blend.","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/metadata","group":"Media","description":"Dimensions, format, channels, density, EXIF orientation, alpha.","auth":"api_key","example":"{ \"width\": 800, \"height\": 450, \"format\": \"jpeg\", \"channels\": 3, \"hasAlpha\": false }","billing":"billable"},{"method":"GET","path":"/v1/cv/stats","group":"Media","description":"Per-channel min/max/mean/stdev + entropy + dominant color.","auth":"api_key","example":"{ \"entropy\": 7.21, \"sharpness\": 1.4, \"dominant\": \"#8a6f4b\", \"isOpaque\": true }","billing":"billable"},{"method":"GET","path":"/v1/cv/video-info","group":"Video","description":"Dimensions, fps, frame count, duration of a video URL. (video tier — answers 503 until its dedicated container is deployed; stated here so nobody finds out from a bill)","auth":"api_key","example":"{ \"width\": 1920, \"height\": 1080, \"fps\": 30, \"frames\": 5400, \"duration\": 180 }","billing":"billable"},{"method":"GET","path":"/v1/cv/frame","group":"Video","description":"Extract one frame at a timestamp as PNG. (video tier — answers 503 until its dedicated container is deployed; stated here so nobody finds out from a bill)","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/keyframes","group":"Video","description":"Contact sheet of evenly-spaced frames (one PNG grid). (video tier — answers 503 until its dedicated container is deployed; stated here so nobody finds out from a bill)","auth":"api_key","billing":"billable"},{"method":"GET","path":"/v1/cv/scene-changes","group":"Video","description":"Timestamps where the picture cuts (sampled histogram correlation). (video tier — answers 503 until its dedicated container is deployed; stated here so nobody finds out from a bill)","auth":"api_key","example":"{ \"sceneChanges\": [{ \"t\": 4.0, \"confidence\": 0.87 }], \"sampled\": true }","billing":"billable"},{"method":"GET","path":"/v1/cv/motion-timeline","group":"Video","description":"Motion score over time (sampled frame differencing) + the peak moment. (video tier — answers 503 until its dedicated container is deployed; stated here so nobody finds out from a bill)","auth":"api_key","example":"{ \"timeline\": [{ \"t\": 1.5, \"score\": 0.031 }], \"peak\": { \"t\": 4.0, \"score\": 1.0 } }","billing":"billable"},{"method":"POST","path":"/v1/cv/pipeline","group":"Pipeline","description":"Chain named catalog ops across engines in one request — resize on sharp, then detect on opencv, then composite back on sharp — without your server round-tripping the bytes in between. One op per step, image-kind ops only, up to 8 steps.","auth":"api_key","billing":"billable"},{"method":"POST","path":"/v1/cv/raw","group":"Raw bridge","description":"Invoke ANY OpenCV function by name — the whole cv2 API (1000+ functions across cv2 and its submodules) behind one endpoint. Body is a step list; values use the {$image}/{$imageData}/{$array}/{$const} codec; steps chain so class-based APIs (SIFT, ORB, BFMatcher) run in one call. GUI/disk/model-file functions are blocked; each call is subprocess-isolated.","auth":"api_key","example":"{ \"result\": { \"$array\": [[0.1], [0.24], [0.4], [0.24], [0.1]], \"shape\": [5, 1], \"dtype\": \"float64\" } }","billing":"billable"}],"cliCommands":["cv ops","cv run <op> --src <url> [--<param> <value>…] [--out file]","cv blur-score <src>","cv detect-bounds <src>","cv trim <src> --out out.png","cv raw <fn> --set arg=value…  (or --json for the full step list)"],"enabled":false},{"id":"games","name":"Games","tagline":"A bring-your-own-game multiplayer engine: rooms, presence, chat, relay, leaderboards.","description":"Drop multiplayer into your site without writing netcode — for YOUR game, not ours. You write the rules as a pure GameDefinition (init/reduce/tick) and register it in the browser SDK; cheapass provides rooms with join codes, quick-match, presence, chat, a 20 Hz WebSocket relay for realtime games, a durable input log for turn-based ones, and per-tenant all-time leaderboards. We never see your game's code. We do see the bill.","pitch":"A bring-your-own-game multiplayer engine: rooms with join codes, quick-match, presence, chat, a 20 Hz WebSocket relay, AI opponents, and per-tenant leaderboards. You write the rules as a pure reducer; one of your players' browsers referees. We relay, store, and bill.","basePath":"/v1/games","category":"build","highlights":[{"label":"You bring","items":["the game (a pure reducer)","the renderer"]},{"label":"We bring","items":["rooms + quick-match","presence + chat","the relay","leaderboards"]}],"status":"available","alwaysOn":false,"howItWorks":"A game is a pure reducer you keep in your own codebase — init(players) seeds the state, reduce(state, input) applies moves, an optional tick(state, dt) advances real-time simulations, and score() settles the ending. Register it with the SDK (createGamesClient({ games: [myGame] })) and every player's client can run it. One client at a time — the authority, host first, migrating automatically if they vanish — actually does: it reduces everyone's inputs, drives the AI opponents, and reports the result. Realtime games ride a per-room WebSocket relay (a Cloudflare Durable Object) that forwards inputs to the authority and fans its ~20 Hz state frames back out; turn-based games use a durable input log on Convex that the authority folds into state. Honesty corner: because the referee is a browser you control, cheapass can't verify scores — it validates shape and roster, and every leaderboard is scoped to your tenant, so the only leaderboard you can cheat on is your own. Browser clients authenticate with a publishable site key (POST /v1/games/site-keys) plus per-player tokens issued at join — your secret API key never ships to a browser. When a game ends, per-player results are recorded and your tenant's all-time best scores update.","getStarted":["cheapass login — authenticate the CLI.","cheapass games site-keys create — mint the publishable key your web client embeds.","npm i @cheapass/games — the engine SDK (rooms, live sync, the authority runner).","Write your GameDefinition (type, minPlayers/maxPlayers, realtime, init/reduce/tick/score/ai).","createGamesClient({ deploymentUrl, siteKey, identity, games: [myGame] }).quickMatch(\"my-game\") — you're in a room.","curl \"$API/v1/games/leaderboard/my-game\" -H \"Authorization: Bearer $KEY\" — your tenant's all-time top list."],"useCases":["Add a playable game section to an editorial site (the flagship consumer is Teh Grep's /games, which brings its own Pong).","Ship a two-player game with AI fallback so a lone visitor always has an opponent — the bots run wherever the authority client runs.","Run casual tournaments off the built-in results history and per-tenant best-score leaderboards."],"endpoints":[{"method":"GET","path":"/v1/games/rooms","group":"Rooms","description":"This account's rooms, newest first.","auth":"api_key"},{"method":"GET","path":"/v1/games/leaderboard/:gameType","group":"Scores","description":"All-time leaderboard for one of your game types. Tenant-scoped (game types are yours, not a global registry): authenticate with an API key, or read publicly with ?siteKey=gspk_….","auth":"api_key","example":"[{ \"name\": \"Ada\", \"bestScore\": 7, \"gamesPlayed\": 12, \"wins\": 9 }]"},{"method":"POST","path":"/v1/games/site-keys","group":"Site keys","description":"Mint a publishable site key (gspk_…) for browser game clients.","auth":"api_key"},{"method":"GET","path":"/v1/games/site-keys","group":"Site keys","description":"List this account's site keys (publishable — returned whole).","auth":"api_key"}],"cliCommands":["games rooms [--status <status>]","games leaderboard <gameType>","games site-keys [create <name>]"],"enabled":false},{"id":"podcasts","name":"Podcasts","tagline":"Upload audio segments, get a hosted podcast: stitched episodes, RSS, CDN, analytics.","description":"The layer between a pile of MP3s and the apps listeners actually use. Create a show, post episodes as an ordered list of audio segments — intro, stories, segues, outro — and we stitch them into one episode file, host it on a CDN, render a standards-compliant RSS feed, and count downloads. Submit the feed URL to Apple, Spotify, and YouTube once; every future episode flows out on its own, because podcast distribution is pull-based and we do the being-pulled-from part.","pitch":"Upload audio segments in order — intro, stories, outro — and we stitch them into one episode file, host it on a CDN, render standards-compliant RSS, and count downloads. The layer between a pile of MP3s and the apps listeners actually use.","basePath":"/v1/podcasts","category":"publish","highlights":[{"label":"Segment kinds","items":["intro","story","segue","outro","body"]},{"label":"Podcasting 2.0","items":["chapters","transcripts"]},{"label":"Baked in","items":["Xing seek header","silence padding","auto episode numbers","download analytics"]},{"label":"Distribution","items":["Apple detection","Spotify detection (via connect)","submit links"]}],"status":"available","alwaysOn":false,"howItWorks":"A show is the container (title, artwork, category, language — everything directories read from the feed's channel). An episode is an ordered list of segment URLs: publicly fetchable, same-format MP3s (CBR, one sample rate — e.g. ElevenLabs mp3_44100_128). POST the segments and the pipeline concatenates them at the MPEG frame level (ID3 containers stripped, encoder metadata frames dropped — no re-encode, so no quality loss), computes exact duration from the frame count, uploads the artifact to CDN storage, and flips the episode to ready. Assembly also prepends a Xing header with a 100-entry seek table (accurate scrubbing everywhere) and stamps each segment's exact startSeconds/durationSeconds onto the episode, which powers two Podcasting 2.0 documents for free: segments with a title become chapters (episodes/<id>/chapters.json, linked from the feed via <podcast:chapters>), and segments with a transcript become a timed JSON transcript (episodes/<id>/transcript.json, via <podcast:transcript>). Segments can also carry padBeforeSeconds/padAfterSeconds — silence inserted as pre-encoded silent frames (~26 ms granularity, still no re-encode). The feed at /v1/podcasts/feeds/<slug>.xml renders RSS 2.0 with iTunes tags (episodic type, per-item episodeType/link, auto-assigned episode numbers); an episode appears once it is assembled, not a draft, and its publishAt has passed — scheduling is just a future publishAt, no cron involved. Enclosure URLs point at /v1/podcasts/audio/<id>.mp3, which counts the download and 302-redirects to the CDN, so per-episode daily download counts come for free. Mixed formats across segments — sample rate, MPEG version, or channel mode (mono vs stereo; some decoders abort at a mid-stream switch) — fail the episode with mixed_audio_formats instead of producing broken audio. In broadcast terms, a typical episode is: intro stinger (theme), billboard (spoken open), the rundown's stories with bumper stingers between them, sign-off, outro stinger — the segment kinds (intro/story/segue/outro/body) are the positions those pieces occupy. Directory distribution: no directory exposes a public submission API (Apple's Delegated Delivery is gated to enrolled hosting partners; Spotify and YouTube are dashboard-only), so submission is one manual paste of the feed URL — the distribution endpoints hand you prefilled submit links, then DETECT when the show goes live: Apple via the public iTunes Search API (matched exactly on feedUrl), Spotify via the Web API using a Spotify connection from the connect product (matched on title + publisher). YouTube has no reliable lookup — mark it manually.","getStarted":["cheapass login — authenticate the CLI.","curl -X POST \"$API/v1/podcasts/shows\" -d '{ \"slug\": \"my-show\", \"title\": \"My Show\", \"description\": \"…\", \"author\": \"Me\", \"ownerEmail\": \"me@example.com\" }' — create the show.","curl -X POST \"$API/v1/podcasts/shows/<showId>/episodes\" -d '{ \"title\": \"Episode 1\", \"description\": \"…\", \"segments\": [{ \"kind\": \"intro\", \"sourceUrl\": \"https://…/intro.mp3\" }, { \"kind\": \"body\", \"sourceUrl\": \"https://…/main.mp3\", \"title\": \"The story\", \"transcript\": \"Spoken text…\" }, { \"kind\": \"outro\", \"sourceUrl\": \"https://…/outro.mp3\" }] }' — the pipeline stitches + hosts it; titled segments become chapters, transcribed ones a transcript.","Submit the show's feedUrl to Apple Podcasts, Spotify, and YouTube Studio once — new episodes distribute automatically."],"useCases":["Serve an AI-generated daily news digest as a real podcast (the flagship consumer is Teh Grep Daily: intro + article narrations + segues + outro).","White-label podcast hosting behind your own product with a few API calls.","Automate a show whose episodes are assembled from reusable pieces (standard intro/outro around fresh content)."],"endpoints":[{"method":"POST","path":"/v1/podcasts/shows","group":"Shows","description":"Create a show (slug, title, description, author, ownerEmail; optional artworkUrl, siteUrl, category, language, explicit).","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/shows","group":"Shows","description":"List this account's shows.","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/shows/:id","group":"Shows","description":"One show.","auth":"api_key"},{"method":"PATCH","path":"/v1/podcasts/shows/:id","group":"Shows","description":"Update show metadata (slug is immutable — it is the feed URL).","auth":"api_key"},{"method":"POST","path":"/v1/podcasts/shows/:id/episodes","group":"Episodes","description":"Create an episode from ordered segment URLs (intro/story/segue/outro/body). Returns 202 with status processing; assembly stitches + hosts the audio asynchronously. Per segment: title → a Podcasting 2.0 chapter, transcript → a timed transcript entry, padBefore/padAfterSeconds → inserted silence (no re-encode). episodeNumber auto-assigns (next per show) when omitted; linkUrl becomes the feed item's <link>.","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/shows/:id/episodes","group":"Episodes","description":"List a show's episodes, newest first (all states).","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/episodes/:id","group":"Episodes","description":"One episode, including assembly status and audio metadata.","auth":"api_key"},{"method":"PATCH","path":"/v1/podcasts/episodes/:id","group":"Episodes","description":"Update episode metadata; sending new segments re-runs assembly IN PLACE (same guid + episode number — the feed shows one item, re-stitched). This is how a publisher re-runs a day's episode without creating a duplicate.","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/shows/:id/distribution","group":"Distribution","description":"Per-directory distribution state (Apple / Spotify / YouTube) with prefilled submit links and the feed URL to paste. Submission itself is manual — no directory exposes a public submission API.","auth":"api_key"},{"method":"POST","path":"/v1/podcasts/shows/:id/distribution/check","group":"Distribution","description":"Detect live listings and persist them: Apple via the public iTunes Search API (exact feedUrl match), Spotify via the Web API using your `connect` Spotify connection (title + publisher match). Returns the refreshed rows plus per-directory notes when detection was skipped.","auth":"api_key"},{"method":"PATCH","path":"/v1/podcasts/shows/:id/distribution/:directory","group":"Distribution","description":"Manually mark a directory (e.g. YouTube after Studio RSS setup, or record a submission as pending).","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/episodes/:id/analytics","group":"Analytics","description":"Download counts: total plus per-UTC-day breakdown.","auth":"api_key"},{"method":"GET","path":"/v1/podcasts/episodes/:id/chapters.json","group":"Public","description":"Podcasting 2.0 chapters (json+chapters) built from the episode's titled segments and their assembly-stamped timeline — linked automatically from the feed via <podcast:chapters>.","auth":"none","example":"{ \"version\": \"1.2.0\", \"chapters\": [{ \"startTime\": 9.038, \"title\": \"Introduction\" }, { \"startTime\": 47.229, \"title\": \"First story headline\" }] }"},{"method":"GET","path":"/v1/podcasts/episodes/:id/transcript.json","group":"Public","description":"Podcasting 2.0 JSON transcript built from the segments' spoken text, timed by the assembly's frame-counted offsets — linked from the feed via <podcast:transcript>.","auth":"none","example":"{ \"version\": \"1.0.0\", \"segments\": [{ \"startTime\": 9.0, \"endTime\": 45.8, \"body\": \"Welcome to the show…\" }] }"},{"method":"GET","path":"/v1/podcasts/feeds/:slug.xml","group":"Public","description":"The show's RSS feed — RSS 2.0 + iTunes tags (episodic type, copyright, per-item episodeType/link/episode numbers) + Podcasting 2.0 chapter/transcript links. This is the URL you submit to directories.","auth":"none"},{"method":"GET","path":"/v1/podcasts/audio/:episodeId.mp3","group":"Public","description":"Counting audio redirect — increments the day's download count and 302s to the CDN file. Used as the feed's enclosure URL.","auth":"none"}],"cliCommands":["podcasts shows","podcasts shows create '<json>'","podcasts episodes <showId>"],"enabled":false},{"id":"platform","name":"Platform","tagline":"The account & meta API under every product: identity, keys, catalog, health, OpenAPI.","description":"Everything that isn't a product but that every integration touches: check who a key belongs to, list and mint API keys, browse and enable products programmatically, read platform health, pull the OpenAPI spec, and run the CLI's device-code login flow. These endpoints power the dashboard and the CLI — documented here so your own tooling can use them too.","pitch":"The account plumbing under every product: API keys, usage, billing, and status. Not a product you buy — the surface you manage the others through.","basePath":"/v1","category":"build","status":"available","alwaysOn":true,"howItWorks":"All platform endpoints live directly under /v1 and authenticate with the same Bearer API key as the products (except the device-auth pair, which is how a key gets issued in the first place). GET /v1/users/me answers 'who am I and what do I have enabled' — the standard first call for a new integration and the cheapest way to validate a key. The api-keys endpoints manage keys with per-product scoping, and the projects endpoints manage optional project scopes: a key assigned to a project resolves that project's connections (falling back to Global) and answers to its kill switches. Keys work fine without one - Global is just the absence of a project. The credentials endpoints (POST/GET/DELETE /v1/credentials) are a vault for third-party secrets: store one per connector provider you plan to use, encrypted at rest and never returned by any read — plumbing for connector-backed endpoints; none ship yet, this just stores the keys so they're ready when they do. The products endpoints expose the same catalog the homepage renders — including per-endpoint docs (GET /v1/products/:id/endpoints/:slug returns one endpoint's generated schema + example) — so agents and CLIs can discover capabilities at runtime. Products auto-enable on your first call to them (plan gates still apply), so you rarely need to enable anything by hand; POST /v1/products/:id/enable remains for pre-flighting, and DELETE /v1/products/:id is an explicit kill switch that blocks a product until re-enabled. /v1/status mirrors the public status page as JSON, and /v1/status/feeds, /v1/status/data, and /v1/status/sports break health down per upstream source (every shared source gets its own tracked status). /v1/openapi.json is the whole platform as an OpenAPI 3 document, one import away from Postman or codegen; GET /llms.txt is the same docs as an llmstxt.org index for LLMs. POST /mcp serves the same catalog to MCP clients: point one at it and it gets search_docs, get_product, and get_endpoint with no key, plus try_endpoint, which calls a real endpoint on your behalf by forwarding the Authorization header sent on the /mcp request itself — no separate credential to configure. The device flow (POST /v1/auth/device, then poll) is RFC-8628-style: the CLI shows a code, you approve it at /device, the poll returns a fresh API key.","getStarted":["cheapass login — the CLI runs the device flow for you and stores the key.","curl \"$API/v1/users/me\" -H \"Authorization: Bearer $KEY\" — validate the key + see enabled products.","curl \"$API/v1/products\" — browse the catalog programmatically (no auth needed).","curl -X POST \"$API/v1/products/podcasts/enable\" -H \"Authorization: Bearer $KEY\" — optional: products auto-enable on first use, but you can pre-flight one here."],"useCases":["A CI job or agent that validates its key and enables the products it needs before running.","Generate a typed client from /v1/openapi.json instead of hand-writing requests.","Monitor platform health from your own status page via /v1/status."],"endpoints":[{"method":"GET","path":"/v1/users/me","group":"Identity","description":"The authenticated account: id, email, plan, enabled products. The standard 'is my key valid' call.","auth":"api_key"},{"method":"GET","path":"/v1/api-keys","group":"Keys","description":"List this account's API keys (prefixes only — secrets are shown once at mint).","auth":"api_key"},{"method":"POST","path":"/v1/api-keys","group":"Keys","description":"Mint a key, optionally scoped to specific products and/or assigned to a project.","auth":"api_key"},{"method":"PATCH","path":"/v1/api-keys/:id","group":"Keys","description":"Assign or move a key between projects. Body requires projectId: a project id to move the key there, or null to move it back to Global — the field itself is required (there is no 'omit it, nothing changes' form).","auth":"api_key"},{"method":"DELETE","path":"/v1/api-keys/:id","group":"Keys","description":"Revoke a key.","auth":"api_key"},{"method":"POST","path":"/v1/credentials","group":"Credentials","description":"Store a third-party secret (API key/token) for a connector provider, encrypted at rest. Returns the credential's id/provider/label/createdAt — never the secret again.","auth":"api_key"},{"method":"GET","path":"/v1/credentials","group":"Credentials","description":"List this account's stored credentials (provider/label/createdAt only — secrets are never returned).","auth":"api_key"},{"method":"DELETE","path":"/v1/credentials/:id","group":"Credentials","description":"Remove a stored credential.","auth":"api_key"},{"method":"GET","path":"/v1/projects","group":"Projects","description":"List this account's projects.","auth":"api_key"},{"method":"POST","path":"/v1/projects","group":"Projects","description":"Create a project - an optional scope for keys, connections, and product kill switches. By default a project only overrides what it sets (everything else falls back to Global); pass allowGlobalFallback: false for strict isolation - its keys then see only the project's own connections.","auth":"api_key"},{"method":"PATCH","path":"/v1/projects/:id","group":"Projects","description":"Rename a project, change its description/icon, or flip its Global-fallback setting.","auth":"api_key"},{"method":"DELETE","path":"/v1/projects/:id","group":"Projects","description":"Delete a project. Its keys and connections fall back to Global; its kill switches are removed. Nothing is destroyed except the grouping.","auth":"api_key"},{"method":"GET","path":"/v1/products","group":"Catalog","description":"The full product catalog with per-endpoint documentation — the same data the docs render, machine-readable.","auth":"none"},{"method":"GET","path":"/v1/products/:id","group":"Catalog","description":"One product's catalog entry.","auth":"none"},{"method":"GET","path":"/v1/products/:id/endpoints/:slug","group":"Catalog","description":"One endpoint from a product's catalog entry, addressed by its stable slug (the same slug /docs/<product>/endpoints/<slug> uses) — enriched with its generated request/response JSON Schema and a runnable example.","auth":"none"},{"method":"POST","path":"/v1/products/:id/enable","group":"Catalog","description":"Enable a product explicitly. Optional — products auto-enable on your first call to them (plan gates still apply); this endpoint remains for pre-flighting and for re-enabling after a disable. ?project=<id> targets one project's switch (default: the calling key's scope).","auth":"api_key"},{"method":"DELETE","path":"/v1/products/:id","group":"Catalog","description":"Disable a product — an explicit kill switch. Calls to it 403 until re-enabled; auto-enable-on-use won't override it. ?project=<id> switches off only that project (default: the calling key's scope; a Global disable applies account-wide). Always-on products (social, assistant) can't be disabled.","auth":"api_key"},{"method":"GET","path":"/v1/status","group":"Health","description":"Platform health as JSON — the public status page's data.","auth":"none"},{"method":"GET","path":"/v1/status/feeds","group":"Health","description":"Per-source health for every feed in the shared catalog: status, last fetch, consecutive failures, reliability.","auth":"none"},{"method":"GET","path":"/v1/status/data","group":"Health","description":"Per-source health for the data product's upstream datasets — which are live, which still 501.","auth":"none"},{"method":"GET","path":"/v1/status/sports","group":"Health","description":"Per-league health for the shared sports-scores catalog: status, live-game count, consecutive failures, last poll — the same per-source pattern as /v1/status/feeds.","auth":"none"},{"method":"GET","path":"/v1/openapi.json","group":"Health","description":"The entire platform as an OpenAPI 3 document — import into Postman or generate a client.","auth":"none"},{"method":"GET","path":"/llms.txt","group":"Health","description":"The docs as an llmstxt.org index — point an LLM at this instead of scraping the website.","auth":"none"},{"method":"GET","path":"/llms-full.txt","group":"Health","description":"The entire platform reference as one LLM-readable markdown file — every product, every endpoint, schemas and runnable examples included.","auth":"none"},{"method":"POST","path":"/mcp","group":"Health","description":"A Streamable-HTTP MCP server: search_docs, get_product, and get_endpoint read the same catalog as the docs site and are keyless; try_endpoint calls a real endpoint on your behalf, forwarding this request's own Authorization header — no separate credential needed.","auth":"none"},{"method":"POST","path":"/v1/auth/device","group":"Device auth","description":"Start the CLI device-code flow: returns a user code + verification URL.","auth":"none"},{"method":"POST","path":"/v1/auth/device/poll","group":"Device auth","description":"Poll the device flow; once the code is approved at /device, returns a fresh API key.","auth":"none"}],"cliCommands":["login","whoami","products [id] [endpoint]","products enable <id>","products disable <id>","keys","keys create <name> [--project <id>]","credentials list","credentials add <provider> [--label <label>]","credentials remove <id>","usage [--project <id>] [--hours <n>]","projects","projects create <name> [--isolated]","mcp"],"enabled":true},{"id":"gate","name":"Gate","tagline":"One password, any site. Put a shared password in front of a preview, a staging box, a private page.","description":"Create a named gate with a shared password, then protect any site with it: the site prompts for the password, exchanges it for a signed token at a keyless endpoint, and unlocks. Tokens are HMAC-signed so the protected site can verify them offline with the gate's secret — no per-request API call. One password, reusable across as many sites as you like, changeable in one place.","pitch":"Password-protect a preview or a private page without a paid Vercel add-on or rolling your own auth. One shared password, a keyless check endpoint, offline-verifiable tokens. Reuse it on every site you own.","basePath":"/v1/gate","category":"build","status":"available","alwaysOn":true,"howItWorks":"Each gate stores a PBKDF2 hash of your shared password and a random HMAC signing secret (returned to you once). A protected site prompts the visitor, POSTs { gate, password } to the PUBLIC /v1/gate/check (no API key), and gets back a short signed token { gate id, expiry }. The site stores the token (cookie/localStorage) and admits the visitor. To gate each request it either verifies the token OFFLINE with the signing secret (an HMAC check, zero network) or calls the keyless /v1/gate/verify. Wrong passwords are throttled per gate with a lockout. Change the password or rotate the secret from one place and every site using the gate follows.","getStarted":["cheapass gate set escher-preview --password 'hunter2' — create a gate (prints its id + signing secret once).","On the protected site: POST /v1/gate/check with { \"gate\": \"<id>\", \"password\": \"…\" } to get a token.","Verify the token offline with the signing secret (HMAC-SHA256), or POST /v1/gate/verify to check it server-side."],"useCases":["Password-protect Vercel/Netlify preview or staging deployments without a paid protection add-on.","Put a single shared password in front of several private sites and manage it in one place.","Gate a client demo or an internal tool with a password you can rotate instantly everywhere."],"endpoints":[{"method":"GET","path":"/v1/gate","group":"Gates","description":"List your gates (never returns password hashes or secrets).","auth":"api_key"},{"method":"POST","path":"/v1/gate","group":"Gates","description":"Create a gate. Returns the signing secret ONCE — store it to verify tokens offline.","auth":"api_key","example":"{ \"id\": \"…\", \"slug\": \"escher-preview\", \"signingSecret\": \"gsk_…\", \"ttlSeconds\": 604800 }"},{"method":"PATCH","path":"/v1/gate/:id","group":"Gates","description":"Change the password, label, token lifetime, or rotate the signing secret.","auth":"api_key"},{"method":"DELETE","path":"/v1/gate/:id","group":"Gates","description":"Delete a gate. Any tokens it minted stop verifying.","auth":"api_key"},{"method":"POST","path":"/v1/gate/check","group":"Unlock","description":"PUBLIC (no API key): exchange the shared password for a signed access token.","auth":"none","example":"{ \"token\": \"eyJ….sig\", \"expiresAt\": 1765000000000 }"},{"method":"POST","path":"/v1/gate/verify","group":"Unlock","description":"PUBLIC (no API key): server-side token verification for sites that don't hold the secret.","auth":"none","example":"{ \"valid\": true, \"expiresAt\": 1765000000000 }"}],"cliCommands":["gate list","gate set <slug> --password <pw> [--label <label>] [--ttl <seconds>]","gate rotate <id>","gate rm <id>"],"enabled":true}]}