{"capability":"scheduler","description":"Run recurring or on-demand scripts in fresh sandboxes. Direct calls use refundable scheduler escrow; scheduler.pay.zeroclick.io uses marketplace access plus usage reporting and keeps only a seller-local lifetime execution cap. Start at `getting_started`; full narrative guide at /llms.txt.","getting_started":{"minimal":"Plain repeatable script: POST /api/v1/tasks with { schedule?, runtime: 'python'|'node', source, entrypoint, max_runtime_sec, max_sibling_calls: 0, max_amount_usdc_micros }. Direct calls also send X-Pay-Amount-Micros == max_amount plus Idempotency-Key; ZeroClick calls use marketplace auth and treat max_amount as a local lifetime cap. Your code has internet access and common libs preinstalled. New here? GET /llms.txt.","full_walkthrough":["1) GET /api/v1/rates for the per-run rates (and, ONLY if your code calls the model, pick an `llm` profile + note its max_cost_usdc_micros_per_run).","2) Size max_amount_usdc_micros >= estimated_ceiling_micros * desired runs. It is refundable escrow on direct calls and a non-refundable seller-local execution cap on ZeroClick calls.","3) Write `source` — any code, internet access. Check `sandbox.preinstalled` before adding `dependencies`. Optional: generate_text(...) (needs `llm`) / call(...) (a first-party sibling, or any paid marketplace capability — see the `siblings` doc for the discover-then-cap pattern + the `tool_list`/`max_sibling_spend_micros` gating).","4) POST /api/v1/tasks. Direct: include X-Pay-Amount-Micros == max_amount_usdc_micros + a unique Idempotency-Key. ZeroClick: marketplace auth/access applies automatically.","5) POST /api/v1/tasks/:id/runs to fire a run immediately (synchronous, blocks until it finalizes) — use it to trigger an on-demand run or to test the cron you just set up before trusting the schedule.","6) Monitor GET /api/v1/tasks/:id/runs + /ledger; extend via /fund. DELETE refunds direct escrow, while ZeroClick has no seller-held buyer balance to refund."]},"key_concepts":{"idempotency_key":"Required on POST /api/v1/tasks. Same key + same body = same task. Same key + different body = 409 IDEMPOTENCY_CONFLICT (the canonical request hash compares cron, source, runtime, deps, tool_list, env keys, end_at, caps).","max_amount_micros":"Lifetime task ceiling in micros (1 USDC = 1_000_000 micros). Direct rail: buyer USDC escrowed by Scheduler and refunded when unused. ZeroClick rail: seller-local execution cap only; marketplace billing/credits are authoritative and DELETE creates no seller refund.","estimated_ceiling_micros":"Per-run worst case the server RESERVES before each run = max_runtime_sec*daytona_rate + max_sibling_calls*sibling_fee + dep_install_term + (if llm set) the profile max_cost_usdc_micros_per_run + overhead, where dep_install_term = (count of declared dependencies)*dep_install_budget_per_pkg_sec*dep_install_per_second_micros. Echoed on create. Size your deposit: max_amount_usdc_micros >= estimated_ceiling_micros * (runs you want before a top-up). The default LLM reserves are tuned for repeated cron work (quick $0.05/run, standard $0.15/run, bulk $0.15/run for high-output jobs); lower them with llm.max_cost_usdc_micros_per_run if needed, and every package in `dependencies` adds to the ceiling, so don't declare libs already preinstalled (see the `sandbox` section). If max_amount < ceiling the task pauses before its first run. GET /api/v1/rates carries all the rate constants + llm_profiles.","tool_list":"OPTIONAL allowlist of targets (first-party sibling names AND/OR marketplace capabilityIds) the task may `call(...)`. Set it to gate to a fixed set; LEAVE IT UNSET/open with `max_sibling_calls` > 0 to allow any capability — an open list REQUIRES `max_sibling_spend_micros` so \"open\" stays bounded. With `max_sibling_calls` 0/omitted an unset tool_list is simply deny-all (no cap needed). Removed entries break script behavior — additions OK via PATCH, removals require clone+recreate.","max_sibling_spend_micros":"Per-run ceiling (USDC micros, integer as string or number) on total sibling/paid-capability spend. REQUIRED whenever `tool_list` is unset/open AND `max_sibling_calls` > 0 (so an open task can never spend more than this per run); optional but recommended otherwise. This is the per-run sibling reservation the server holds against escrow — first-party flat fees and paid-capability actuals both draw it down.","env":"BYO env vars for the sandbox. KMS-encrypted at rest, never echoed in any GET response. Rotate via PUT /api/v1/tasks/:id/env.","webhook":"A task can expose a secret-authed webhook so external callers (CI, dashboard buttons, Zapier) can trigger a synchronous run without a wallet. Enable: PUT /api/v1/tasks/:id/webhook (owner-gated) — returns a whsec_ secret ONCE; store it immediately. Trigger: POST /api/v1/tasks/:id/webhook with Authorization: Bearer <secret> — runs synchronously, billed from the task deposit, in-flight=1 (concurrent → 429). Rotate via PUT again; disable via DELETE /api/v1/tasks/:id/webhook. Keep the secret server-side — exposing it client-side lets anyone spend your deposit.","schedule":"Optional. Provide a discriminated union — either { cron, timezone } OR convenience { every: minute|hourly|daily|weekly, at: \"HH:MM\", dayOfWeek: mon|tue|..., timezone } (server compiles the convenience form to a 5-field cron). OMIT it entirely for an on-demand task: no cron, never auto-runs, executes only when you call POST /tasks/:id/runs (sync) or /trigger (async); stays active until deleted.","runtime":"python (Python 3.12) or node (Node 22). Both ship with curated common libs preinstalled.","ledger":"GET /api/v1/tasks/:id/ledger identifies its billing rail. Direct: authoritative buyer-money ledger where SUM(amount) equals max_amount - cumulative_cost and collapses to 0 on terminal. ZeroClick: operational audit rows only; marketplace usage is authoritative.","pause_recovery":"When the remaining escrow/cap is below the per-run ceiling, dispatch pauses the task. GET /api/v1/tasks/:id returns a recovery_hint. POST /fund extends direct escrow or the ZeroClick local cap. DELETE refunds unused direct escrow; ZeroClick has no seller-held balance to recover."},"sandbox":{"runtimes":"Your `source` runs in a fresh sandbox every run. runtime='python' (Python 3.12) or 'node' (Node 22). entrypoint is the filename your source is written to (e.g. main.py / index.js).","preinstalled":"These libs are already in the image — import them directly, do NOT list them in `dependencies` (every declared package adds the dep-install term to estimated_ceiling_micros). python: requests, httpx, feedparser, beautifulsoup4, lxml, python-dateutil, pyyaml, pillow. node: axios, node-fetch, cheerio, date-fns, yaml, sharp. Only use `dependencies` for packages NOT in this list.","network":"Sandboxes have outbound internet access — your code can call external HTTPS APIs (e.g. a news or data API) directly.","output":"Each run's stdout + stderr are captured and returned by GET /api/v1/runs/:id (truncated; full logs via logs_url). IMPORTANT: if you declare `dependencies`, the pip/npm install output is written to stdout BEFORE your program's output — so if you parse a result out of stdout, print a unique sentinel line (e.g. `RESULT <json>`) and read that, rather than assuming stdout is only your program.","helpers":"Four helpers are available as GLOBALS in your source — no import, no API keys, no setup. Call them directly: generate_text(...) (built-in LLM; needs an `llm` profile), call(...) (invoke a first-party sibling OR any paid marketplace capability — see the `siblings` field for the discover-then-cap pattern), store_artifact(...) (durable cdn hosting; needs `artifacts.enabled`), request_continuation(...) (schedule an immediate follow-up run — see the `continuation` field). The explicit `from withzero_sdk import generate_text, call, store_artifact, request_continuation` (python) / `import { generateText, call, storeArtifact, requestContinuation } from './withzero-sdk.js'` (node) also works but is optional.","llm":"generate_text(prompt, model_alias?, model?, system?, max_output_tokens?, temperature?, reasoning?) → { status, body: { text, model, usage: { input_tokens, output_tokens }, cost_usdc_micros, metadata } }. Global — no import. Enable by setting `llm` on the task (choose a profile; GET /api/v1/rates → llm_profiles lists models, default model, and caps). Cost: each call is billed and the profile max_cost_usdc_micros_per_run is reserved into estimated_ceiling_micros — fund max_amount_usdc_micros accordingly.","siblings":"call(capabilityId_or_sibling, path, body?, method?, max_price_micros?) → { status, body } — invoke another Zero capability through the per-run proxy. Global — no import. TWO kinds of target: (1) a FIRST-PARTY sibling by name (urlshortener, cdn, forms, screenshot, inbox) — free to wire, billed a flat per-call fee, no max_price_micros needed; allowed when listed in `tool_list` (or when `tool_list` is left open). (2) ANY PAID marketplace capability by its capabilityId — pass it as the first arg AND set max_price_micros (the most USDC micros you accept for this one call). BOTH x402 and MPP capabilities are supported (the scheduler escrow wallet pays x402 from a Base working float and MPP from Tempo escrow). DISCOVER-THEN-CAP: the sandbox has internet egress, so query api.zero.xyz yourself first (capabilities.get / search expose cost + paymentMethods[].protocol + priceObserved) — pick a capability, choose an accepted ceiling, then pass it as max_price_micros. The proxy resolves the capabilityId server-side (SSRF-safe), reserves your cap, pays via whichever protocol the capability requires from the scheduler escrow wallet, and refunds the difference — you never handle keys or sign anything. ALLOWLIST: when the task sets `tool_list`, every target (sibling name or capabilityId) must be in it; leave `tool_list` UNSET/open (with `max_sibling_calls` > 0) to call any capability, but then `max_sibling_spend_micros` is REQUIRED on the task and bounds total per-run paid spend. Per-run count is also capped by max_sibling_calls. Errors: PRICE_CAP_EXCEEDED (a paid challenge exceeded your max_price_micros — nothing was spent), PRICE_UNKNOWN (missing/invalid max_price_micros or capabilityId for a paid target), CAPABILITY_PROTOCOL_UNSUPPORTED (capability offers no supported payment protocol — needs x402 or MPP), SIBLING_PAYMENT_FAILED (resolution/settlement failed), SIBLING_NOT_ALLOWED (target not in `tool_list`), CALL_CAP_EXCEEDED (max_sibling_calls hit).","artifacts":"store_artifact(name, data, content_type?, stable?) → { status, body: { url, path, bytes } } — host durable bytes/text on the cdn and get a public URL back (HTML pages, images, reports). Global — no import. Set `artifacts.enabled: true` on the task; bounded by per-run upload/byte caps. By default each store returns a NEW immutable URL. Pass stable=true for a FIXED bookmarkable URL per (task, name) that each run OVERWRITES — the way to build a cron-maintained live page / pseudo-app whose UI updates every run at one link (e.g. store_artifact(\"dashboard.html\", html, content_type=\"text/html\", stable=True)).","continuation":"request_continuation(delay_sec?) → { status, body: { accepted, consecutive_continuations, max_consecutive_continuations } } — chunk work larger than one run's wall-clock cap. Global — no import. Pattern: process what fits in the cap, persist your progress externally (the sandbox is stateless), call request_continuation(), exit 0. The scheduler starts a fresh, fully-billed run within ~1 minute of this run finishing — granted ONLY when this run exits cleanly (a crash or timeout waits for the natural cron occurrence instead). Back-to-back continuations are capped (body.max_consecutive_continuations; body.accepted=false once the cap is hit) and every follow-up passes the normal funding check, so an underfunded task pauses rather than looping. delay_sec (0-600, default 0) postpones the follow-up.","example_python":"# helpers are globals — no import needed\nr = generate_text(\"Summarize today's AI news in one sentence.\", max_output_tokens=80)\nurl = store_artifact(\"news.html\", \"<h1>\" + r[\"body\"][\"text\"] + \"</h1>\", content_type=\"text/html\")[\"body\"][\"url\"]\nprint(\"RESULT\", url)  # task: { llm: { profile: \"quick\" }, artifacts: { enabled: true } }"},"lifecycle":{"discover":[{"method":"GET","path":"/api/v1","paid":false,"proof_required":false,"summary":"This catalog."},{"method":"GET","path":"/api/v1/rates","paid":false,"proof_required":false,"summary":"Per-second + per-call rates and hard caps. Use before create to predict cost."},{"method":"GET","path":"/api/v1/_siblings","paid":false,"proof_required":false,"summary":"Sibling capabilities you can list in tool_list."},{"method":"GET","path":"/openapi.json","paid":false,"proof_required":false,"summary":"Full OpenAPI 3 spec with request/response schemas. Per-operation `x-payment-info` carries sku/amount/method — the canonical paywrap discovery surface."}],"create_and_manage":[{"method":"POST","path":"/api/v1/tasks","paid":true,"proof_required":false,"summary":"Create a task (charge-intent). X-Pay-Amount-Micros must equal max_amount_usdc_micros. Idempotency-Key required. Body: schedule, runtime, source, entrypoint, max_runtime_sec, max_sibling_calls, max_amount_usdc_micros, optional tool_list/dependencies/env/notify_email/end_at. notify_email gets a best-effort completion email per scheduled/triggered run (sync /runs + /webhook return the result inline instead)."},{"method":"POST","path":"/api/v1/tasks/:id/fund","paid":true,"proof_required":false,"summary":"Top up max_amount_micros to extend runway. X-Pay-Amount-Micros must equal top_up_amount_micros. Auto-resumes paused_insufficient_funds. Idempotency-Key required. Must reuse the same MPP channelId as the original /tasks call (CHANNEL_MISMATCH otherwise)."},{"method":"POST","path":"/api/v1/tasks/clone","paid":false,"proof_required":true,"summary":"Return a fresh task spec from an existing task id. Env values masked. Use this + POST /tasks to change immutable fields."},{"method":"PATCH","path":"/api/v1/tasks/:id","paid":false,"proof_required":true,"summary":"Update notify_email, end_at, tool_list (additions only), max_sibling_spend_micros (raises only), or max_runtime_sec (raises only; 409 RUN_IN_FLIGHT while a run is claimed/running). Other fields are immutable — IMMUTABLE_FIELD on attempt."},{"method":"PUT","path":"/api/v1/tasks/:id/env","paid":false,"proof_required":true,"summary":"Replace BYO env. KMS re-encrypted; never echoed."},{"method":"POST","path":"/api/v1/tasks/:id/pause","paid":false,"proof_required":true,"summary":"Manually pause. Future runs skipped until resume."},{"method":"POST","path":"/api/v1/tasks/:id/resume","paid":false,"proof_required":true,"summary":"Resume a paused task. 409 if balance < ceiling."},{"method":"PUT","path":"/api/v1/tasks/:id/webhook","paid":false,"proof_required":true,"summary":"Enable (or rotate) the secret-authed webhook for this task. Returns { secret: \"whsec_...\" } once — store it immediately, it is never returned again. Old secret is invalidated on rotate."},{"method":"DELETE","path":"/api/v1/tasks/:id/webhook","paid":false,"proof_required":true,"summary":"Disable the webhook for this task. The whsec_ secret is immediately invalidated."}],"execute_and_inspect":[{"method":"GET","path":"/api/v1/tasks/:id","paid":false,"proof_required":true,"summary":"Fetch task (balances, status, next_run_at, lifetime cost). env never returned."},{"method":"POST","path":"/api/v1/tasks/:id/runs","paid":false,"proof_required":true,"summary":"Run the task immediately and synchronously — same primitive as cron, blocks until the run finalizes, billed like a normal run. Use it to trigger an on-demand run or to test a cron you just set up before trusting the schedule. Does NOT advance next_run_at."},{"method":"POST","path":"/api/v1/tasks/:id/trigger","paid":false,"proof_required":true,"summary":"Trigger a run asynchronously (non-blocking) — pulls next_run_at to now; the dispatch tick runs it within ~1 min. Returns immediately. Use for event-driven/webhook triggers. No-op on non-active tasks. (POST /tasks/:id/runs is the synchronous variant.)"},{"method":"POST","path":"/api/v1/tasks/:id/webhook","paid":false,"proof_required":false,"summary":"Secret-authed webhook trigger — no wallet required. Authorization: Bearer <whsec_secret>. Runs the task synchronously, billed from the task deposit. In-flight=1; concurrent calls return 429. Enable the secret via PUT /tasks/:id/webhook (owner). See key_concepts.webhook."},{"method":"GET","path":"/api/v1/tasks/:id/runs","paid":false,"proof_required":true,"summary":"List recent runs (slim rows, cursor-paginated)."},{"method":"GET","path":"/api/v1/runs/:id","paid":false,"proof_required":true,"summary":"Deep-dive single run with truncated stdout/stderr + Cloud Logging URL."},{"method":"GET","path":"/api/v1/owner/usage","paid":false,"proof_required":true,"summary":"Cross-task spend rollup for the calling wallet."},{"method":"GET","path":"/api/v1/account","paid":false,"proof_required":false,"summary":"Key-authed self-service account read (plan, status, period, credit balance, tasks_active/runs_lifetime counters, and the full key list). Requires a zsk_ API key, not a wallet proof."},{"method":"GET","path":"/api/v1/account/keys","paid":false,"proof_required":false,"summary":"List the calling account API keys (id, prefix, created_at, revoked_at, last_used_at). Requires a zsk_ API key."}],"audit_and_close":[{"method":"GET","path":"/api/v1/tasks/:id/ledger","paid":false,"proof_required":true,"summary":"Append-only money-movement audit trail per task. Each row records a charge, refund, run cost, or dust forfeit with the on-chain tx hash."},{"method":"DELETE","path":"/api/v1/tasks/:id","paid":false,"proof_required":true,"summary":"Close the task: cancels future runs, refunds (max_amount - cumulative) via on-chain refundCharge, writes refund ledger row. Idempotent on tasks.refund_tx_hash."},{"method":"GET","path":"/api/v1/account/ledger","paid":false,"proof_required":false,"summary":"Cursor-paginated audit trail over ALL of the account credit/spend movements (grant + reversal + cost_finalize), newest first — not scoped to one task. Requires a zsk_ API key."},{"method":"DELETE","path":"/api/v1/account/keys/:id","paid":false,"proof_required":false,"summary":"Revoke one of the account API keys. Idempotent (200 whether or not it was already revoked); 404 for a key belonging to a different account. Self-revocation with either key is fine — the standard rotation is mint new via ZeroClick, then revoke old."}]},"error_codes":["API_KEY_INVALID","API_KEY_NOT_FOUND","VALIDATION_ERROR","INVALID_CRON","UNSUPPORTED_RUNTIME","UNSUPPORTED_DEPENDENCY","UNKNOWN_SIBLING","INSUFFICIENT_FUND_AMOUNT","IDEMPOTENCY_CONFLICT","CHANNEL_MISMATCH","IMMUTABLE_FIELD","IMMUTABLE_TOOL_REMOVAL","TASK_NOT_FOUND","TASK_DELETED","TASK_COMPLETED","TASK_PAUSED_INSUFFICIENT_FUNDS","TASK_PAUSED_MANUAL","TASK_TERMINATED","SIBLING_NOT_ALLOWED","CALL_CAP_EXCEEDED","PRICE_CAP_EXCEEDED","PRICE_UNKNOWN","CAPABILITY_PROTOCOL_UNSUPPORTED","SIBLING_PAYMENT_FAILED","TOKEN_EXPIRED","VOUCHER_BEHIND_CUMULATIVE","RATE_LIMIT","INTERNAL","ROUTE_NOT_FOUND"],"discovery_urls":{"route_index":"https://scheduler.withzero.xyz/api/v1","openapi":"https://scheduler.withzero.xyz/openapi.json","rates":"https://scheduler.withzero.xyz/api/v1/rates","siblings":"https://scheduler.withzero.xyz/api/v1/_siblings","guide":"https://scheduler.withzero.xyz/llms.txt"}}