{"service": "Tidepool \u2014 always-on Python pods for AI agents", "description": "Deploy persistent Python apps (pods) that run as always-on servers with auth, payments, admin, and file storage built in.", "install": "pip install tidepool", "cli": "`tidepool init <name>` \u2192 `tidepool dev` \u2192 `tidepool deploy`. Also: `quickstart`, `push`, `pull`, `status`, `kill`, `pods`, `credits`. Run `tidepool <cmd> --help` for details.", "getting_started": "1. `pip install tidepool` 2. `tidepool init my-app && cd my-app` 3. Edit main.py (use `import tp`, register routes with `@tp.route`, set `tp.auth`/`tp.payments`/`tp.admin`) 4. `tidepool dev` to test locally at http://localhost:8000 5. `tidepool --url https://tidepool.sh register --email you@example.com` 6. `tidepool deploy` \u2014 live at https://my-app.tidepool.sh in ~30s 7. Custom domain: `GET /api/domains/search?query=<keyword>` to find a domain, `POST /api/domains/buy` with `{\"domain\": \"example.com\"}` to buy it, then `PATCH /api/pods/<hash>` with `{\"custom_domain\": \"example.com\"}` to attach it. DNS is auto-configured for Tidepool-purchased domains (takes ~1 hour; poll `GET /api/pods/<hash>` and watch `custom_domain_status` change from `pending` to `active`).", "quickstart": "`tidepool quickstart` launches Claude Code to build a full demo app. Or run it yourself: `claude --model claude-opus-4-6 --dangerously-skip-permissions \"Build a Substack clone as a Tidepool pod. Run 'curl https://tidepool.sh/api' first to learn the platform. Use tp.auth (with email verification+password), tp.payments ($5/mo paid tier), and tp.admin (restricted to author). Seed users: author@example.com / test123 (paid subscription), freereader@example.com / test123, paidreader@example.com / test123. Seed 5-7 real articles in tp.db (keys like post:slug), mix of free and paid, rendered with tp.markdown(). Topic: short essays on the Puritans \u2014 contrasting what they're known for in popular culture with what they actually said, ending with what that says about modern society. The author's view allows inline editing (title, body, free/paid, draft/published, image placement and scaling); readers see the same view minus controls. We have provided one hero image per article topic at https://tidepool.sh/quickstart/{romantic_love,family_inheritance,entrepreneurship,debt,land_ownership,marriage,child_rearing,political_life,class_differences}.jpg \u2014 download and use to build the clone. Style: serif fonts, no emojis. The dignity of a First Things or Atlantic style print magazine, avoid 2010s Wordpress style. Landing page: use a compact two-column grid of equally-sized article cards on desktop \u2014 no oversized featured/hero card. Keep it dense and scannable. Mobile-friendly. Hero images for each article. Test with 'tidepool dev' after each change. Verify: article cards on landing page, article detail, auth flow, paywall on paid articles, admin panel, author editing.\"`. See https://tidepool.sh/quickstart for details.", "pricing": "1,000 credits = $20. Standard pod: 150 credits/mo ($3.00/mo). Plus pod: 250 credits/mo ($5.00/mo). 25 free on signup.", "deploy": "`tidepool deploy` auto-discovers files in the current directory. Use `--env plus` for 2048MB RAM (default: standard, 1024MB). Source files are capped at 10MB \u2014 for large data (datasets, models, media), put files in `tp_data/files/` and they'll be uploaded to `tp.files` automatically. To exclude files/dirs from deploy and push, create a `.tpignore` file (like `.gitignore`): `data/` ignores a directory, `*.csv` ignores by pattern. Always ignored: `tp_data/`, `__pycache__/`, `.git/`, `venv/`, dotfiles, `build_log.*`.", "endpoints": {"POST /api/register": "Register agent. Body: `{\"email\"}` \u2192 `{api_key, billing_url}`. Agent is immediately active with free credits. Verify email within 24 hours (or make a purchase) to keep account active.", "GET /api/me": "Agent info (`hash`, `email`, `status`, `credits`, `autoreload`)", "PATCH /api/me": "Update agent. Body: `{\"email\"?, \"autoreload\"?}`", "GET /api/me/stripe-connect": "Check Stripe Connect status for accepting payments in pods", "POST /api/me/feedback": "Submit feedback. Body: `{\"category\": \"bug\"|\"feature-request\"|\"compliment\"|\"misc\", \"title\": \"<string>\", \"text\": \"<string>\"}`. Title: <=25 chars, <=5 words. Text: <=3500 chars, <=700 words. Rate limit: 5/day.", "POST /api/me/stripe-connect": "Create Stripe Connect onboarding link. Returns URL \u2014 open in browser to complete setup. Once done, `tp.payments` in your pods will process real charges. One-time setup: POST this endpoint, open the URL, complete Stripe onboarding (bank account, identity). All future pods automatically use your connected account.", "POST /api/pods": "Create pod. Body: `{\"files\": [{\"name\": \"main.py\", \"content\": \"...\"}], \"subdomain\"?, \"env\"?: \"standard\"|\"plus\"}`. `env` selects the pod size by name or hash (default: \"standard\" 1024MB, \"plus\" 2048MB). `subdomain` becomes `<subdomain>.tidepool.sh` (defaults to pod hash if omitted). Files max 10MB total (source code only). Binary files: `{\"name\", \"content\": \"<base64>\", \"encoding\": \"base64\"}`. For large data use `tp.files` (see `deploy` key above).", "GET /api/pods": "List pods", "GET /api/pods/<hash>": "Pod detail \u2014 includes `status_log` (boot errors, handler errors, status changes) for debugging", "PATCH /api/pods/<hash>": "Update pod. Body: `{\"files\"?, \"secrets\"?, \"status\"?, \"replicas\"?: int (1-10), \"custom_domain\"?: \"example.com\"}`. `custom_domain`: DNS auto-configured for Tidepool-purchased domains, otherwise returns `dns_instructions`; set to `null` to remove. Files are merged by name \u2014 only send the files you want to add/update.", "GET /api/pods/<hash>/db": "Export all `tp.db` key-value pairs as JSON", "PUT /api/pods/<hash>/db": "Import `tp.db`. Body: `{\"db\": {key: value, ...}}`. Replaces ALL existing data.", "PATCH /api/pods/<hash>/db": "Merge `tp.db` keys. Body: `{\"db\": {key: value, ...}}`. Only updates specified keys, leaves others intact.", "GET /api/pods/<hash>/files/<name>": "Download an R2 file (raw bytes)", "PUT /api/pods/<hash>/files/<name>": "Upload an R2 file (raw bytes body, not JSON). Max 250MB per request.", "DELETE /api/pods/<hash>/files/<name>": "Delete an R2 file", "DELETE /api/pods/<hash>": "Delete pod (destroys VM and files)", "GET /api/domains": "List your purchased domains with expiration dates, auto-renew status, and attached pod.", "GET /api/domains/search?query=<keyword>": "Search domains across 20 TLDs. Availability checked via WHOIS (parallel, ~1s). Pricing from Porkbun. Results sorted: available first, then by price. `available`=`true`/`false`/`null` (`null` = inconclusive). Purchase re-confirms via Porkbun.", "POST /api/domains/buy": "Buy a domain. Body: `{\"domain\": \"example.com\", \"autoreload\"?: true}`. Deducts credits. Auto-renews annually (charges credits). If not enough credits and `autoreload` is `true`, charges card on file first. Returns instructions to attach to a pod.", "PATCH /api/domains/<domain>": "Update domain settings. Body: `{\"auto_renew\": true/false}`. Domains auto-renew 14 days before expiry by default.", "POST /api/domains/<domain>/renew": "Manually renew a domain for 1 year. Body: `{\"autoreload\"?: true}`. Deducts credits. If not enough credits and `autoreload` is `true`, charges card on file first.", "DELETE /api/domains/<domain>": "Release a domain. Detaches from any pod, removes DNS records, deletes the record. Domain will not be renewed.", "GET <subdomain>.tidepool.sh": "Pod web app (`?format=json` for state)", "POST <subdomain>.tidepool.sh": "Post to pod app"}, "runtime": {"IMPORTANT": "Use `import tp` at the top of any `.py` file that needs the `tp` object. This works in `main.py` and in any helper modules you create.", "EXECUTION MODEL": "`main.py` runs once at startup to configure the server. Set up `tp.auth`, `tp.payments`, `tp.admin`, seed data, register routes with `@tp.route()` and static pages with `tp.page()`. The server dispatches requests directly to handlers \u2014 `main.py` is never re-executed.", "@tp.route(path, methods)": "decorator that registers a handler for the given path. Path patterns use `:param` syntax (e.g. `/post/:slug`). Handler receives `(req, **params)` where `req` is a Request object. Return: `str` \u2192 200 HTML, `dict/list` \u2192 200 JSON, `int` \u2192 status code, `tuple(body, status)` \u2192 body + status, `tuple(body, status, headers)` \u2192 full response, `None` \u2192 303 redirect to same path, `generator` \u2192 SSE stream.", "SSE (Server-Sent Events)": "Return a generator from a route handler to stream SSE. Each yielded value (`dict`, `list`, or `str`) is sent as a `data:` event. Client: `new EventSource(\"/path\")`. Max 100 concurrent SSE connections per pod.", "@tp.background(seconds=0)": "decorator that registers a background task. The function receives the `tp` object as its only argument. `seconds=0` (default): runs once at startup. `seconds>0`: runs in a recurring loop. Max 5 per pod. Errors are caught and logged. Use for one-time migrations, periodic syncing, sending digests, updating caches, cleanup.", "tp.page(path, html)": "register a static page. String path + html, or dict of `{path: html}`. Served from memory, no handler call.", "Request object": "Passed to route handlers. Attributes: `path`, `method`, `query` (dict), `user` (dict or None), `body` (dict), `files` (dict of `{name, content_type, size}` for multipart uploads \u2014 file data is auto-saved to `tp.files`).", "tp.auth": "dict or preset string. Two presets: (1) `tp.auth = \"paywall\"` \u2014 for apps where payment comes first. No signup form. Accounts are auto-created when users pay via Stripe checkout. Return logins use magic link (email-only, no password). Pair with `tp.payments`. (2) `tp.auth = \"standard\"` \u2014 traditional email/password signup with email confirmation, password reset, and magic link option. After setting a preset, customize with e.g. `tp.auth[\"required\"] = [\"/dash/*\"]` or `tp.auth[\"theme\"] = {accent: \"#e74c3c\"}`. Or skip presets and pass a full dict: `{required: [\"/dash/*\"], signup: True, reset: True, magic_link: True, oauth: [\"google\"]}`. Keys: `required` (list of path patterns to gate), `signup` (bool), `signup_confirm` (bool, default True), `reset` (bool), `magic_link` (bool), `magic_link_only` (bool \u2014 hides password field), `oauth` (list, e.g. `[\"google\"]`), `theme` (`{accent: \"#color\", css: \"...\"}` or `{page: \"<html>...{content}...{title}...\"}`). Google OAuth: put `google_client_id` and `google_client_secret` in `tp_data/secrets.json`. Redirect URI: `https://yourdomain.com/_auth/oauth/google/callback`.", "tp.payments": "dict \u2014 Stripe Connect commerce. Set `{products: [{id: \"pro\", name: \"Pro\", price: 500, recurring: \"month\"}]}`. `price` is in cents. `recurring`: `\"month\"`/`\"year\"` for subscriptions, omit for one-time. Users pay at `/_pay/<id>`, manage subscriptions at `/_pay/portal`. In dev mode purchases are simulated instantly. To collect real revenue in prod, the user must first run `tidepool stripe-connect` to connect their Stripe account (one-time setup, no code changes needed). Remind the user to do this before deploying if the app uses `tp.payments`.", "tp.admin": "dict \u2014 admin panel at `/_admin/`. Set `{models: {post: {fields: {title: \"string\", body: \"text\"}, display: [\"title\"]}}}`. Field types: `string`, `text`, `bool`, `number`, `date`, `file`, `choice:a,b,c`. Optional: `users: [\"email\"]` to restrict access. If not set, auto-inferred from `tp.db` keys matching the pattern `model:id`.", "tp.create_user(email, password, **extra)": "create user with hashed password. Idempotent. Extra kwargs (e.g. `subscriptions={}`) stored on user record.", "tp.users()": "returns all users (password hashes excluded).", "tp.db": "key-value store. Methods: `set(k,v)`, `setdefault(k,v)`, `get(k)`, `delete(k)`, `keys(prefix?)`, `prefix(pre, limit=1000, reverse=False, offset=0)`, `count(prefix?)`, `all()`, `clear()`. Convention: use `model:id` keys (e.g. `post:my-slug`) for admin auto-discovery.", "tp.files": "persistent file storage (R2 in prod, 50GB limit). Methods: `write(name, data)`, `read(name)`, `list()`, `delete(name)`. Served at `/_files/<name>`. Multipart uploads auto-save here. For large data (images, datasets, media), use `tp.files` at runtime \u2014 deploy-time files are capped at 10MB and are for source code only.", "tp.email(to, subject, body, html?, attachments?)": "send email. Rate limited: 5/handler call, 100/day.", "tp.http": "HTTP client (same as `requests`). `tp.http.post(url, json={\"key\": \"val\"})`, `tp.http.get(url, params={\"q\": \"hi\"})`. Methods: `get`, `post`, `put`, `patch`, `delete`, `head`. All kwargs from `requests` work: `json=`, `data=`, `headers=`, `params=`, `timeout=`. Rate limited: 200 req/60s. For heavy API work, use `import requests` directly.", "tp.secrets": "read-only dict \u2014 set at deploy time via the API. Never hardcode API keys in source files. In pod code: `tp.secrets[\"my_key\"]`. In local scripts outside pods (pipelines, etc.): `json.load(open(\"tp_data/secrets.json\"))`. Both read from the same file in dev.", "tp.state": "dict \u2014 public app state. Readable at `?format=json`.", "tp.publish(data)": "update the pod's public JSON state.", "tp.docs()": "returns this documentation dict.", "tp.pod_id": "string \u2014 the pod hash.", "STATIC FILES": "Files in a `static/` directory alongside `main.py` are served at `/static/<path>`.", "TEMPLATING": "Jinja2 is pre-installed. `from jinja2 import Environment, FileSystemLoader; env = Environment(loader=FileSystemLoader(\"templates\"), autoescape=True)`. Render: `env.get_template(\"page.html\").render(posts=p)`.", "tp.markdown(text)": "convert markdown to HTML. Includes tables, fenced code, footnotes (uses markdown `\"extra\"` extensions).", "LIBRARIES": "call `tp.libraries()` for a dict of `{name: version}` of all pre-installed packages.", "EJECT MODE": "`tidepool eject` copies `tp_runtime.py`, `tp_server.py`, `tp_backend.py`, `tp_templates/` into your project. Edit them freely \u2014 dev/prod/push/pull auto-detect eject mode when `tp_server.py` exists. Undo: delete those files."}, "auth": "Authorization: Bearer tp_xxxxx. Rate limit: 60 req/min. IMPORTANT: Your API key grants full access to all pods, secrets, database, and files. Treat it like a password \u2014 do not commit it to repos or share it publicly.", "api_hits": 377}