{"openapi":"3.1.0","info":{"title":"Sato Hub public API","version":"1.1.0","summary":"Read access to the onchain agent directory, its change feed, and the Agent Passport registry.","description":"Public, unauthenticated read endpoints for the Sato Hub directory.\n\nLicense: Catalog data CC-BY-4.0 — attribution: data by satohub.ai. Scores/liveness are informational signals, not safety or quality guarantees.\n\nScores and liveness are informational signals computed from public evidence. They are not safety ratings, audits, or claims about returns. `verification_status` distinguishes self-reported from independently checked; self-reported is never presented as verified.\n\nEvery shape this API returns is also published as a versioned JSON Schema (draft 2020-12) at https://satohub.ai/schemas — catalogue at https://satohub.ai/schemas/index.json. They are referenced from components.schemas below, and v1 is additive-only: a breaking change becomes v2 at a new URL. Validate with additional properties allowed.\n\nThe same data is available to agents over MCP (Streamable HTTP, JSON-RPC) at https://satohub.ai/api/mcp — documented at https://satohub.ai/mcp. That endpoint is not described here because it is not REST.","contact":{"name":"Sato Hub","url":"https://satohub.ai"},"x-guidance":"Free: search and read the onchain-agent catalog (/api/export/index.json, /api/changes, /api/resolve) and use the MCP server at /api/mcp. Paid over x402 (USDC on Base, no account or key): GET /api/x402/history/{slug} for one listing's daily Sato Score record, GET /api/x402/agent-economy for the weekly agent-economy series. The Sato Score measures how open, active and verifiable a project is; it is not a safety or returns grade.","x-status":{"page":"https://satohub.ai/status/sato-hub","json":"https://satohub.ai/api/status.json","feed":"https://satohub.ai/status/feed.xml","note":"Success ratios and latencies of Sato Hub's own hourly checks. Not a service-level agreement."},"license":{"name":"CC-BY-4.0","url":"https://creativecommons.org/licenses/by/4.0/"}},"servers":[{"url":"https://satohub.ai"}],"tags":[{"name":"directory","description":"The catalog of components agents are built from."},{"name":"sync","description":"Incremental mirroring."},{"name":"skills","description":"Crypto-relevant agent skills with static disclosures — what a skill declares and does. Descriptions, never safety verdicts."},{"name":"agent-economy","description":"Weekly on-chain measurements per venue, chain and lifecycle stage. Nothing in these responses may be summed — read each venue's unit first."},{"name":"registry","description":"Registered onchain agents and their passports."},{"name":"badges","description":"Embeddable badges — SVG, or the shields.io endpoint-badge JSON. No key, hotlinkable."},{"name":"resolve","description":"Map a repository URL or package name to its listing. Exact match only."},{"name":"check","description":"Sato Check: what a package, MCP server, skill, repo or x402 endpoint can do with an agent's keys and funds. A profile describes what was read and run, with dates — never a safety rating."},{"name":"preflight","description":"One check before an agent installs, connects or pays. A verdict names what was checked and when — never that anything is safe."},{"name":"route","description":"Sato Route. A route is a RECOMMENDATION: the venue is chosen by liveness, verification state and quoted price as those fields read on the date in `checked_at`, and `chosen_by` names every field it was read from. It is not a claim that a venue is best, safe or sound, and a quote is not a fill. The Sato fee is a parameter on the aggregator's own quote — charged inside the swap transaction by that router, disclosed on the response before anything is signed, and nothing is charged on a failed, reverted or unsigned trade. Nothing in this tag signs a transaction, holds a key, or takes custody of funds: the caller receives calldata and signs it, or does not."},{"name":"satobot","description":"Sato Bot. A goal in plain words in, a BUILD PLAN out: the goal restated, a stack of real directory listings each carrying its Sato Score and `sato_url`, a deploy spec and a Preflight verdict per item, the first action when the goal implies one, the open questions and the next steps. Nothing in a plan is invented, `null` is unknown rather than zero, and a plan is a recommendation assembled from Sato Hub's own records at `checked_at` — not a security review, not a quality judgment, not a statement about returns. This tag holds no keys and signs nothing: a swap first-action is quoted at a NOMINAL size, never the caller's, and a launch first-action is a config the caller reads and signs itself."},{"name":"signing","description":"Response signing. Every Preflight verdict and every Sato Route decision is returned with a detached Ed25519 signature — `Sato-Signature` over the exact response bytes plus `Sato-Signed-At`, and the same signature under `meta.signature` for clients that only see the JSON. Keys are published as a JWK Set; the construction is published beside them. A valid signature proves origin, integrity and the stamped time. It is not a safety claim and it does not make a verdict more true. `meta.signature: null` with no header means the deployment has no signing key — unknown, never a placeholder."},{"name":"swap","description":"Sato Swap. One call chooses the venue, discloses that venue's own fee sentence verbatim, checks four targets — both tokens, the venue endpoint and the recipient — against the caller's policy, simulates the transaction, and then returns either a recommendation or an UNSIGNED transaction. NON-CUSTODIAL: nothing in this tag holds a key, signs, or broadcasts. UNKNOWN REFUSES BY DEFAULT and every refusal names the rule and the reading that produced it, so \"we did not check\" and \"we checked and it is fine\" never look alike. A refusal arrives as HTTP 200 carrying `withheld.reason`: it is an answer, not a failure, and retrying it unchanged will refuse again. A recipient with no Agent Passport is `no_record` — the ordinary case, never a finding against the address — and there is no list of trusted counterparties here. Receipts record what was checked; they are never a claim a trade filled, and they are never summed into volume."},{"name":"claims","description":"Maintainers proving control of a listing they own."},{"name":"demand","description":"What agents ask for, as ranks and shares of demand. Never traffic totals — no call, caller or session count appears in this API."},{"name":"projects","description":"Private agent projects and their immutable sealed versions. Owner-only: every operation resolves the owner from the Privy bearer, never from a body field, and a caller who does not own the object gets 404 — never 403, which would confirm it exists."},{"name":"listings","description":"Published agent packages. A listing is a public VIEW of one sealed, immutable version — allowlisted version fields plus secret-scanned owner fields, bound to the version's digest. Public reads need no auth. Owner operations take ONLY the Privy bearer; a non-owner gets 404. Acquisition modes are `download` and `fork`; there is no deploy or buy."}],"externalDocs":{"description":"The published Sato Hub formats, as JSON Schema.","url":"https://satohub.ai/schemas"},"components":{"schemas":{"export-listing":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/export-listing/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/export-listing/v1.json"}},"change-feed":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/change-feed/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/change-feed/v1.json"}},"deploy-spec":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/deploy-spec.json","externalDocs":{"url":"https://satohub.ai/schemas/deploy-spec.json"}},"agent-manifest":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/agent-manifest/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/agent-manifest/v1.json"}},"preflight-verdict":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/preflight-verdict/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/preflight-verdict/v1.json"}},"route-receipt":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/route-receipt/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/route-receipt/v1.json"}},"signed-envelope":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/signed-envelope/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/signed-envelope/v1.json"}},"build-plan":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/build-plan/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/build-plan/v1.json"}},"skill-disclosure":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/skill-disclosure/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/skill-disclosure/v1.json"}},"score-inputs":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/score-inputs/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/score-inputs/v1.json"}},"score-roots":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/score-roots/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/score-roots/v1.json"}},"swap-quote":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/swap-quote/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/swap-quote/v1.json"}},"swap-receipt":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/swap-receipt/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/swap-receipt/v1.json"}},"release":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/release/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/release/v1.json"}},"custody":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/custody/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/custody/v1.json"}},"facts":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/facts/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/facts/v1.json"}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Two different credentials ride this header, and they are told apart by their shape.\n\n• A SATO API KEY, `sk_sato_…` — optional on every metered endpoint. It is NEVER required: each of those endpoints answers an anonymous caller at the free limits, and a key only raises the caps and attributes the calls to an account. A key may also be sent as `X-Sato-Key`. An unknown or revoked key is served as if no key were present, never refused. Limits and plans: /pricing.\n\n• A PRIVY ACCESS TOKEN for a signed-in account — required by the claim endpoints and the key-management endpoints. A token that is not a Sato key is treated as one of these."},"satoKey":{"type":"apiKey","in":"header","name":"X-Sato-Key","description":"The same optional Sato API key, on its own header. Equivalent to `Authorization: Bearer sk_sato_…`."}}},"paths":{"/api/export/index.json":{"get":{"tags":["directory"],"operationId":"getDirectoryJson","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"The full directory as JSON","description":"Every live listing, with aggregates describing the whole directory (not the slice). Deprecated listings are excluded.","parameters":[{"name":"category","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to one category. Case-insensitive; an unknown value returns an empty slice, not an error."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings supporting this chain."},{"name":"standard","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings implementing this standard."},{"name":"use_case","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to one use case."},{"name":"integration","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings that integrate with a client or framework, e.g. 'claude code', 'cursor'."},{"name":"deploys_as","in":"query","required":false,"schema":{"type":"string"},"description":"Slice by deployment shape, e.g. 'docker', 'npm', 'mcp server'."},{"name":"min_observed_success","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":100},"description":"Only listings whose share of our daily checks that succeeded is at least this percent, over at least 14 observed days. Pass 0 to get every listing that HAS a 14-day record."}],"responses":{"200":{"description":"The catalog.","content":{"application/json":{"schema":{"type":"object","properties":{"schema_version":{"type":"string"},"generated_at":{"type":"string","format":"date-time"},"source":{"type":"string","format":"uri"},"license":{"type":"string"},"count":{"type":"integer","description":"Size of the returned slice."},"aggregates":{"type":"object","description":"Describes the whole directory, not the slice.","properties":{"resources":{"type":"integer"},"chains":{"type":"integer"},"scored":{"type":"integer"},"independently_checked":{"type":"integer"}}},"resources":{"type":"array","items":{"type":"object","description":"One catalog listing. Basic catalog fields only — score history, provenance, trust components and enriched commercial fields are not part of the public export.","properties":{"slug":{"type":"string","description":"Stable identifier. The join key for every other endpoint."},"name":{"type":"string"},"category":{"type":"string"},"subcategory":{"type":"string","nullable":true},"entity_class":{"type":"string","nullable":true,"enum":["resource","agent","reference",null],"description":"resource = something you build WITH; agent = a live onchain agent; reference = editorial."},"resource_type":{"type":"string","nullable":true},"interfaces":{"type":"array","items":{"type":"string"},"description":"mcp | sdk | rest-api | plugin | cli | ui | contract"},"standards":{"type":"array","items":{"type":"string"},"description":"x402 | erc-8004 | erc-8183 | mcp | a2a"},"use_cases":{"type":"array","items":{"type":"string"}},"description_short":{"type":"string"},"website_url":{"type":"string","nullable":true},"github_url":{"type":"string","nullable":true},"docs_url":{"type":"string","nullable":true},"chains_supported":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"open_source_status":{"type":"string","nullable":true},"trust_score":{"type":"integer","nullable":true,"minimum":0,"maximum":100,"description":"Sato Score: how open, active and verifiable the project is, computed from public evidence. NOT a safety, quality, audit or returns grade."},"trust_tier":{"type":"string","nullable":true,"description":"High (>=70) | Medium (>=40) | Low (<40). Null while provisional — an unearned tier is never published."},"provisional":{"type":"boolean","description":"True = not yet assessed; treat trust_tier as absent."},"liveness":{"type":"string","nullable":true,"enum":["Active","Recent","Quiet","Dormant",null],"description":"Recency of observed activity. Active <=30d, Recent <=90d, Quiet <=1y, else Dormant."},"last_activity_at":{"type":"string","format":"date-time","nullable":true},"github_stars":{"type":"integer","nullable":true},"verified_install":{"type":"boolean","description":"The documented install was actually reproduced. Says nothing about runtime safety."},"verification_status":{"type":"string","nullable":true,"description":"Self-Reported and Unverified are NOT verified states. Only Verified and Audited are earned."},"creator_name":{"type":"string","nullable":true,"description":"Who built it, when known."},"deployment_options":{"type":"array","items":{"type":"string"},"description":"How it ships: npm | pip | docker | mcp server | hosted | self-hosted | cli | sdk | claude code plugin …"},"supported_integrations":{"type":"array","items":{"type":"string"},"description":"Clients and frameworks it plugs into: cursor | claude code | claude desktop | windsurf | langchain | openclaw | x402 …"},"observed_success_pct":{"type":"number","nullable":true,"description":"Share of Sato Hub's own daily checks that succeeded over observed_days. NOT uptime: a failed check may be ours, theirs, or the network, and failures before 2026-09-01 are unattributable."},"observed_days":{"type":"integer","nullable":true,"description":"Days of daily observation behind observed_success_pct."},"install_verified_at":{"type":"string","nullable":true,"description":"Date the documented install was reproduced in an ephemeral container by Sato Hub. Null = not reproduced. Says nothing about runtime safety."},"deploy_spec":{"type":"object","nullable":true,"description":"The sato.deploy manifest: runtime, install commands, entry command, what the operator must supply, and whether Sato Hub reproduced the install. Full schema at /schemas/deploy-spec.json, documented at /docs/deploy-spec. deploy_status 'verified' means the documented install ran in an ephemeral container on verification.ran_at — never a safety, audit or quality claim. 'failed' means it was attempted on that date and did not complete, which is a measurement of the documented path rather than a judgement of the project. 'unknown' means we did not attempt it and not_attempted_reason says why. Absent means the same as unknown.","properties":{"runtime":{"type":"string"},"install":{"type":"array","items":{"type":"string"}},"entry":{"type":"string"},"requires":{"type":"array","items":{"type":"string"},"description":"Env vars, keys, wallets, RPC endpoints the operator supplies."},"chains":{"type":"array","items":{"type":"string"}},"license":{"type":"string"},"mcp_native":{"type":"boolean"},"mcp_invocation":{"type":"string","description":"Free-form: a stdio command, a remote URL, or prose where the docs offered no more."},"deploy_clarity":{"type":"string","enum":["high","medium","low"]},"deploy_status":{"type":"string","enum":["verified","self_reported","failed","unknown"]},"source_url":{"type":"string","format":"uri"},"as_of":{"type":"string","format":"date"},"verification":{"type":"object","description":"ran_at, environment, ok, and each check — the evidence behind a verified status."},"install_variants":{"type":"array","items":{"type":"object"},"description":"Alternative lanes. INVARIANT: install/entry above mirror install_variants[0], and the top-level deploy_status describes that lane. Verification is per lane."}}},"install":{"type":"string","nullable":true,"description":"deploy_spec.install[0], flattened. Exists for the CSV, which carries this rather than the object."},"date_added":{"type":"string","format":"date"},"detail_url":{"type":"string","format":"uri"}},"required":["slug","name","category","description_short","status","detail_url"]}}}}}}}}}},"/api/facts.json":{"get":{"tags":["directory"],"operationId":"getSiteFacts","summary":"The public counts every Sato Hub surface quotes, with their date","description":"Listings, chains, scored listings, independently checked listings and MCP tools — the one set of numbers the site, llms.txt, /mcp and the index repo all read — plus `as_of`, the date of the data they were computed from. Quote a number with its as_of. 'Unknown', 'n/a' and 'none' are not counted as chains. These describe the catalogue, never Sato Hub's own traffic. A null as_of is unknown.","responses":{"200":{"description":"The facts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/facts"}}}}}}},"/api/export/score-inputs.json":{"get":{"tags":["directory"],"operationId":"getScoreInputs","summary":"The inputs every published Sato Score is computed from","description":"Per listing, exactly the fields the scoring function reads — nothing else — plus the score currently published, so the number can be recomputed independently. `as_of` is the instant the published scores were computed against and MUST be used as the scorer's clock: several components are functions of elapsed time, so scoring these inputs at a later moment legitimately yields a different number. `as_of: null` means the scoring run stamped no clock in this deployment and the scores cannot be reproduced against these inputs — unknown, not now(). The response is Ed25519-signed (meta.signature, Sato-Signature header) when a signing key is configured. Reproducing a score makes it checkable; it is not a safety, security or audit judgment.","responses":{"200":{"description":"The score inputs.","content":{"application/json":{"schema":{"type":"object","properties":{"schema_version":{"type":"string"},"generated_at":{"type":"string","format":"date-time"},"as_of":{"type":"string","format":"date-time","nullable":true,"description":"The scoring clock. Null means unknown."},"input_keys":{"type":"array","items":{"type":"string"}},"rubric":{"type":"object","description":"Components, their maxima, the tier cutoffs, and where the scorer lives."},"count":{"type":"integer"},"listings":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"category":{"type":"string"},"scoreable":{"type":"boolean","description":"False for non-product categories, which carry no score at all."},"published":{"type":"object","properties":{"trust_score":{"type":"integer","nullable":true},"trust_tier":{"type":"string","nullable":true},"provisional":{"type":"boolean"}}},"inputs":{"type":"object","description":"Keyed by input_keys."}}}}}}}}}}}},"/api/export/index.csv":{"get":{"tags":["directory"],"operationId":"getDirectoryCsv","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"The full directory as CSV","description":"Same rows as the JSON index. Multi-value fields are pipe-joined. Stable column order: slug, name, category, subcategory, entity_class, resource_type, description_short, status, open_source_status, trust_score, trust_tier, provisional, liveness, verification_status, verified_install, github_stars, last_activity_at, date_added, chains_supported, interfaces, standards, use_cases, tags, creator_name, deployment_options, supported_integrations, observed_success_pct, observed_days, install_verified_at, custody_key_access, custody_key_egress, custody_fund_action_count, custody_as_of, install, website_url, github_url, docs_url, detail_url.","parameters":[{"name":"category","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to one category. Case-insensitive; an unknown value returns an empty slice, not an error."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings supporting this chain."},{"name":"standard","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings implementing this standard."},{"name":"use_case","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to one use case."},{"name":"integration","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings that integrate with a client or framework, e.g. 'claude code', 'cursor'."},{"name":"deploys_as","in":"query","required":false,"schema":{"type":"string"},"description":"Slice by deployment shape, e.g. 'docker', 'npm', 'mcp server'."},{"name":"min_observed_success","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":100},"description":"Only listings whose share of our daily checks that succeeded is at least this percent, over at least 14 observed days. Pass 0 to get every listing that HAS a 14-day record."}],"responses":{"200":{"description":"CSV.","content":{"text/csv":{"schema":{"type":"string"}}}}}}},"/api/export/index.ndjson":{"get":{"tags":["directory"],"operationId":"getDirectoryNdjson","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"The full directory as newline-delimited JSON","description":"One catalog record per line. Streamable; suited to jq and warehouse loads.","parameters":[{"name":"category","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to one category. Case-insensitive; an unknown value returns an empty slice, not an error."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings supporting this chain."},{"name":"standard","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings implementing this standard."},{"name":"use_case","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to one use case."},{"name":"integration","in":"query","required":false,"schema":{"type":"string"},"description":"Slice to listings that integrate with a client or framework, e.g. 'claude code', 'cursor'."},{"name":"deploys_as","in":"query","required":false,"schema":{"type":"string"},"description":"Slice by deployment shape, e.g. 'docker', 'npm', 'mcp server'."},{"name":"min_observed_success","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":100},"description":"Only listings whose share of our daily checks that succeeded is at least this percent, over at least 14 observed days. Pass 0 to get every listing that HAS a 14-day record."}],"responses":{"200":{"description":"NDJSON — one ExportEntry per line.","content":{"application/x-ndjson":{"schema":{"type":"string"}}}}}}},"/api/export/skills.json":{"get":{"tags":["skills"],"operationId":"getSkillsJson","summary":"Crypto agent skills with disclosures","description":"Agent skills from ClawHub and skills.sh that touch crypto, each with a static disclosure: hosts contacted, key generation, credential requests, remote-to-shell pipes, tool grants, third-party registration, scheduled persistence. Every flag has evidence lines on the skill's page. A disclosure is not a safety verdict. Current state only.","parameters":[{"name":"registry","in":"query","required":false,"schema":{"type":"string","enum":["clawhub","skillssh","github"]}},{"name":"flag","in":"query","required":false,"schema":{"type":"string"},"description":"Only skills carrying this disclosure flag."},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"slug","in":"query","required":false,"schema":{"type":"string"},"description":"Only skills that target this directory listing."}],"responses":{"200":{"description":"Skills.","content":{"application/json":{"schema":{"type":"object","properties":{"schema_version":{"type":"string"},"license":{"type":"string"},"flag_meanings":{"type":"object"},"count":{"type":"integer"},"skills":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/export/skills.csv":{"get":{"tags":["skills"],"operationId":"getSkillsCsv","summary":"Skills as CSV","parameters":[],"responses":{"200":{"description":"CSV.","content":{"text/csv":{"schema":{"type":"string"}}}}}}},"/api/export/skills.ndjson":{"get":{"tags":["skills"],"operationId":"getSkillsNdjson","summary":"Skills as NDJSON","parameters":[],"responses":{"200":{"description":"NDJSON.","content":{"application/x-ndjson":{"schema":{"type":"string"}}}}}}},"/api/export/x402-verified.json":{"get":{"tags":["x402"],"operationId":"getX402Verified","summary":"x402 endpoints probed for a real HTTP 402, per URL","description":"The rows /numbers/x402-endpoints-that-answer and /x402/verified are built from: every URL probed daily for listings tagged x402, each with declared_as (payment_resource = named as paid in the project's own /.well-known/x402; mcp_endpoint; listing_page = homepage, docs, repo or article), the observed outcome, HTTP status, where the payment payload was read, and the declared network. No payment is sent; null means unknown, never 0. The payload carries its own rules array.","parameters":[],"responses":{"200":{"description":"Per-URL states plus the funnel by kind.","content":{"application/json":{"schema":{"type":"object","properties":{"checked_at":{"type":"string","nullable":true},"window_days":{"type":"integer"},"rules":{"type":"array","items":{"type":"string"}},"funnel":{"type":"object"},"endpoints":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/export/mcp-handshakes.json":{"get":{"tags":["directory"],"operationId":"getMcpHandshakes","summary":"Listed MCP endpoints and the outcome of an anonymous handshake, per endpoint","description":"The rows /numbers/mcp-servers-that-answer is counted from: every listed MCP endpoint URL with the outcome of the daily anonymous initialize + tools/list check (answered, ambiguous, credentials, refused, no_handshake, or not_checked), the tool count, and the time of the reading. counted=true rows form the page's denominator. tools_count null means unknown, never 0. The payload carries its own rules, method and a reproduce command.","parameters":[],"responses":{"200":{"description":"Per-endpoint outcomes plus the counts.","content":{"application/json":{"schema":{"type":"object","properties":{"checked_at":{"type":"string","nullable":true},"run_at":{"type":"string","nullable":true},"rules":{"type":"array","items":{"type":"string"}},"counts":{"type":"object"},"endpoints":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/export/demand.json":{"get":{"tags":["demand"],"operationId":"getDemandShape","summary":"What agents ask for, as ranks and shares","description":"The weekly demand SHAPE: themes agents ask a crypto-agent directory about, ranked by share of demand, with week-on-week movement and the listings each theme resolves to.\n\nTHIS IS DEMAND SHAPE, NOT TRAFFIC. It carries no call count, caller count or session count — for Sato Hub or for anyone else — and no total from which one could be derived. Shares, ranks and percentage-point movements only. If you are looking for how much traffic this site gets, it is not here and will not be.\n\nA share is a share of the demand observed through ONE channel, the Sato Hub MCP endpoint, whose callers are disproportionately machines that found a directory. It is a sample of what agents ask, not a measurement of the market, and it cannot tell you what the market spends or how many agents exist.\n\n`zero_share_pct` is the share of a theme's own asks that came back empty — the gap, stated against ourselves. Probe and health-check traffic is excluded. The payload carries its own `rules` array.","parameters":[],"responses":{"200":{"description":"This week's ranked themes.","content":{"application/json":{"schema":{"type":"object","properties":{"schema_version":{"type":"string"},"generated_at":{"type":"string","format":"date-time"},"week":{"type":"string","description":"ISO week, e.g. 2026-W37."},"window_days":{"type":"integer","nullable":true,"description":"The rolling window the demand was observed over. Consecutive weeks overlap by design."},"compared_with":{"type":"string","format":"date","nullable":true,"description":"The banked demand date the movers are measured against; null before a prior week exists."},"license":{"type":"string"},"rules":{"type":"array","items":{"type":"string"},"description":"How these figures may and may not be used. Part of the payload, not the documentation."},"themes":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"integer"},"id":{"type":"string","description":"Stable theme id. An `other:` prefix is an unclassified ask kept under its own tokens rather than folded away."},"label":{"type":"string"},"classified":{"type":"boolean","description":"False when the ask matched no topic keyword — new demand the taxonomy has not caught up with."},"share_pct":{"type":"number","description":"Share of the window's classifiable demand. Shares across themes sum to ~100."},"zero_share_pct":{"type":"number","description":"Share of THIS theme's asks that returned nothing."},"rank_change":{"type":"integer","nullable":true,"description":"Positive = climbed since last week. Null when the theme is new."},"share_change_pp":{"type":"number","nullable":true,"description":"Change in share, in percentage points."},"is_new":{"type":"boolean"},"examples":{"type":"array","items":{"type":"string"},"description":"The queries themselves, most-asked first. Words, never counts."},"listings":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"sato_url":{"type":"string","format":"uri"}}}}}}},"dropped_since_last_week":{"type":"array","items":{"type":"object"}},"asked_for_and_not_carried":{"type":"array","items":{"type":"string"},"description":"Names agents looked up that the directory does not list. Candidates, not verdicts."}}}}}}}}},"/api/export/adoption.json":{"get":{"tags":["agent-economy"],"operationId":"getAgentEconomyJson","summary":"The measured agent economy for the current week","description":"Weekly measurements of what is actually happening on-chain across the agent venues Sato Hub tracks — identity registries, service registries, a launchpad, a payment rail and account infrastructure — from public chain reads.\n\nTHE RESPONSE CARRIES ITS OWN RULES, in a `rules` array, because the commonest way to misuse this data is to add it up. Each venue declares what ONE UNIT of it is; a registration, a staked service, a token launch and a paid task are not the same object and share no denominator.\n\nA null `value` means UNKNOWN and never zero. A rate with `publishable_rate: false` has too small a sample to quote. `covered_days` is what was actually observed, which is not always `window_days` — public nodes prune history, and on BNB Chain only hours of it are readable at all.\n\nCURRENT WEEK ONLY. The weekly series is not part of the free export.\n\nSolana identity registries are covered as UPPER BOUNDS (program-account counts; unit says so); Solana payment settlement is not covered by any row, and by transaction count x402 mostly settles there. Tempo memo-tagged transfers are MPP-shaped, not proof of MPP payments.","parameters":[{"name":"venue","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one venue."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one chain."},{"name":"stage","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one lifecycle stage."},{"name":"slug","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to measurements attributed to one directory listing."}],"responses":{"200":{"description":"The week's measurements, plus the venue catalogue needed to interpret them.","content":{"application/json":{"schema":{"type":"object","properties":{"schema_version":{"type":"string"},"generated_at":{"type":"string","format":"date-time"},"source":{"type":"string","format":"uri"},"license":{"type":"string"},"week":{"type":"string","nullable":true},"rules":{"type":"array","items":{"type":"string"},"description":"How these numbers may and may not be used. Part of the payload, not the documentation."},"venues":{"type":"array","items":{"type":"object"},"description":"The catalogue: what one unit is per venue, entry cost, measurability, declared overlaps, and how many confirmed contracts back it."},"count":{"type":"integer"},"measurements":{"type":"array","items":{"type":"object","description":"One measurement at the grain (venue, chain, stage, week). NEVER sum these: an ERC-8004 registration, an Olas staked service, a Virtuals launch and a Mech task are four different objects, and `unit` says which one this row counts.","properties":{"venue":{"type":"string","description":"erc8004 | olas | virtuals | x402 | erc4337_accounts | key_management | singularitynet | morpheus"},"chain":{"type":"string","nullable":true,"description":"Null for venue-level rows (a venue with no chain, e.g. key_management)."},"stage":{"type":"string","description":"The lifecycle stage this row counts, e.g. registered, funded, active_beyond_registration, transacting, launched, graduated, settled_to_catalogued_seller."},"week":{"type":"string","description":"ISO week, e.g. 2026-W36."},"week_start":{"type":"string","format":"date"},"platform_host":{"type":"string","nullable":true,"description":"Set on breakdown rows attributing registrations to the platform that produced them."},"belongs_to_slug":{"type":"string","nullable":true,"description":"The directory listing this row is attributed to, when one could be resolved."},"value":{"type":"number","nullable":true,"description":"NULL means UNKNOWN. It never means zero — a measurement failure is not an observation of nothing."},"unit":{"type":"string","description":"What one of these IS. Read it before comparing two venues."},"method":{"type":"string","description":"How the figure was produced, in words. Travels with the number by design."},"evidence_tier":{"type":"integer","enum":[1,2,3],"description":"1 = proof (observing it IS the finding); 2 = indicator (countable but contaminated by human usage); 3 = inference (never assertable)."},"sample_size":{"type":"integer","nullable":true,"description":"Null for exact counts over a whole population."},"denominator":{"type":"integer","nullable":true},"window_days":{"type":"integer","nullable":true,"description":"The window requested."},"covered_days":{"type":"number","nullable":true,"description":"The window ACTUALLY covered. Public nodes prune; these differ, and only this one is comparable across chains."},"publishable_rate":{"type":"boolean","description":"False when the sample is too small to quote as a finding. The row is still returned — it is a real observation, just not a result."},"caveat":{"type":"string","nullable":true,"description":"What limited this specific measurement."},"contract":{"type":"string","nullable":true},"stale_contract":{"type":"boolean","nullable":true,"description":"True when the address confirmation is older than the re-check interval."},"as_of":{"type":"string","format":"date-time"},"venue_url":{"type":"string","format":"uri"}},"required":["venue","stage","week","value","unit","method","evidence_tier","as_of"]}}}}}}}}}},"/api/export/adoption.csv":{"get":{"tags":["agent-economy"],"operationId":"getAgentEconomyCsv","summary":"The measured agent economy as CSV","description":"Same rows as adoption.json. The method and caveat columns are carried deliberately — a spreadsheet is exactly where a number gets separated from its qualification.","parameters":[{"name":"venue","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one venue."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one chain."},{"name":"stage","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one lifecycle stage."},{"name":"slug","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to measurements attributed to one directory listing."}],"responses":{"200":{"description":"CSV.","content":{"text/csv":{"schema":{"type":"string"}}}}}}},"/api/export/adoption.ndjson":{"get":{"tags":["agent-economy"],"operationId":"getAgentEconomyNdjson","summary":"The measured agent economy as newline-delimited JSON","description":"One measurement per line. Streamable.","parameters":[{"name":"venue","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one venue."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one chain."},{"name":"stage","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to one lifecycle stage."},{"name":"slug","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to measurements attributed to one directory listing."}],"responses":{"200":{"description":"NDJSON — one measurement per line.","content":{"application/x-ndjson":{"schema":{"type":"string"}}}}}}},"/api/changes":{"get":{"tags":["sync"],"operationId":"getDirectoryChanges","summary":"What changed since a date","description":"The delta companion to the full export: what was added, what changed, and what was removed since a date, so a consumer does not re-pull the whole index to find the few things that moved.\n\nREAD `coverage` BEFORE RELYING ON THIS. The endpoint reports its own completeness rather than implying it is total: `added` is exact (from date_added); `changed` reflects only what produces a changelog event, so a quiet copy edit will not appear; `removed` is approximate, because there is no deprecated_at column.\n\nTreat it as change NOTIFICATION, not a complete replication log — poll it often, and re-pull the full export periodically to correct drift.\n\nOmitting `since` defaults to a 14-day window.\n\nPREFER NOT TO POLL? The same lanes are delivered by webhook — POST /api/webhooks/subscribe. A delivery is a slice of this feed, signed with your own secret; it carries no field you could not fetch here.","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"ISO date, YYYY-MM-DD. Defaults to 14 days ago."}],"responses":{"200":{"description":"The delta, with an explicit statement of its own coverage.","content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"string","format":"date"},"window_days":{"type":"integer"},"counts":{"type":"object","properties":{"added":{"type":"integer"},"changed":{"type":"integer"},"removed":{"type":"integer"}}},"added":{"type":"array","items":{"type":"object"}},"changed":{"type":"array","items":{"type":"object"},"description":"Grouped per resource, so a consumer syncs per record rather than per event."},"removed":{"type":"array","items":{"type":"object"}},"coverage":{"type":"object","description":"Which parts of this response are complete and which are partial. Read it.","properties":{"complete":{"type":"array","items":{"type":"string"}},"partial":{"type":"array","items":{"type":"string"}},"advice":{"type":"string"}}},"full_export":{"type":"string","format":"uri"}}}}}},"400":{"description":"`since` is not an ISO date."}}}},"/api/freshness":{"get":{"tags":["sync"],"operationId":"getDataFreshness","summary":"How fresh each data surface is","description":"Per data surface (directory activity, news, Sato Score, liveness checks, snapshots, ERC-8004, the weekly agent-economy measurements, x402, LP pools, skills, deploy verification): the last successful collection read from the pipeline ledger, the last attempt and how it ended, the expected cadence, and whether it is late — older than 1.5× that cadence. The same report as the MCP tool onchain_agent_get_data_freshness.\n\n`last_success_at: null` with `late: null` means no successful run is recorded — unknown, never an invented date. An unreadable ledger returns every surface as `unknown`, never fresh. Timestamps and statuses of data jobs only; no usage figure appears here.","parameters":[{"name":"surface","in":"query","required":false,"schema":{"type":"string","enum":["directory_activity","news","erc8004_count","agent_tokens","liveness","sato_score","snapshots","x402_bazaar","x402_answers_daily","erc8004_endpoints_daily","mcp_alive_daily","erc8004_router_pool","passports","erc8004_census","x402_throughput","custody_profiles","agent_economy","x402_settlement","lp_pools","skills","deploy_verification","custody_observed","dead_project_sweep"]},"description":"One surface. Omit for all."}],"responses":{"200":{"description":"The freshness report.","content":{"application/json":{"schema":{"type":"object","properties":{"generated_at":{"type":"string","format":"date-time"},"ledger":{"type":"string","enum":["readable","unavailable"]},"rule":{"type":"string"},"surfaces":{"type":"array","items":{"type":"object","properties":{"surface":{"type":"string"},"what":{"type":"string"},"expected_every":{"type":"string","enum":["daily","weekly"]},"last_success_at":{"type":"string","format":"date-time","nullable":true},"last_attempt_at":{"type":"string","format":"date-time","nullable":true},"last_status":{"type":"string","enum":["ok","failed","running","none","unknown"]},"late":{"type":"boolean","nullable":true},"state":{"type":"string","enum":["fresh","late","never","unknown"]},"next_update":{"type":"string","format":"date-time"},"source":{"type":"string"}}}},"note":{"type":"string"},"source_url":{"type":"string","format":"uri"}}}}}},"400":{"description":"`surface` is not a known surface id."}}}},"/api/webhooks/subscribe":{"post":{"tags":["sync"],"operationId":"subscribeChangeWebhook","summary":"Subscribe an endpoint to change deliveries","description":"The push companion to /api/changes: one POST per lane per day to an endpoint you control, so a mirror does not have to poll. A delivery is a SLICE of that public feed — it carries no field you could not fetch yourself.\n\nYOUR ENDPOINT MUST PROVE ITSELF FIRST. Subscribing triggers a challenge GET carrying `?sato_challenge=<nonce>`; answer it by echoing the nonce (plain text, or {\"challenge\":\"...\"}). Without that echo nothing is stored — otherwise anyone could subscribe anyone else's server to our traffic.\n\nhttps only, and public hosts only: loopback, RFC-1918, CGNAT, link-local and cloud-metadata addresses are refused at subscribe time and re-checked before every delivery.\n\nSIGNING. With a `secret`, every delivery carries `Sato-Webhook-Signature: t=<iso>, v1=<hex>` — HMAC-SHA256 over utf8(t) || 0x0A || canonicalJson(body), the same canonicalisation Sato Hub's Ed25519 response signatures use. Without a secret, deliveries are unsigned and the response says so.\n\nGIVING UP. Three attempts per delivery; after seven consecutive failed deliveries the subscription is disabled and never retried. Re-subscribing revives it and re-runs the challenge.\n\nFull guide, including a verification snippet: https://satohub.ai/api/changes and docs/webhooks.md in the repository.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"https endpoint that will receive deliveries and answer the challenge GET."},"secret":{"type":"string","minLength":16,"maxLength":200,"description":"Your own HMAC secret. Optional; without it deliveries are unsigned. Never echoed back."},"lanes":{"type":"array","description":"Defaults to the five change-feed lanes. `added`/`updated`/`removed` mirror the change feed's lanes; `status` and `verification` are changelog event kinds. `custody` is opt-in: Sato Check `custody.changed` events (a package's key, host or fund behaviour changed between versions) — it describes, never rates.","items":{"type":"string","enum":["added","updated","removed","status","verification","custody"]}}}}}}},"responses":{"201":{"description":"Subscribed. Keep `unsubscribe_token` — it is stateless and not recoverable.","content":{"application/json":{"schema":{"type":"object","properties":{"subscribed":{"type":"boolean"},"id":{"type":"integer"},"url":{"type":"string","format":"uri"},"lanes":{"type":"array","items":{"type":"string"}},"signed":{"type":"boolean"},"unsubscribe_token":{"type":"string","nullable":true,"description":"Null when this deployment has no secret configured to mint one — never a placeholder."},"terms":{"type":"string"}}}}}},"400":{"description":"Bad URL, unknown lane, or a secret that is too short."},"422":{"description":"The endpoint did not echo the challenge."},"503":{"description":"Subscriptions are not configured on this deployment."}}},"delete":{"tags":["sync"],"operationId":"unsubscribeChangeWebhook","summary":"Unsubscribe, token-bound","description":"Pass the `id` and `unsubscribe_token` from the subscribe response. The token is an HMAC over that id and that URL, so it is worthless against any other subscription. A wrong token and a missing subscription return the same 404 — neither answer should let anyone enumerate subscriptions.","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer"}},{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unsubscribed."},"400":{"description":"`id` or `token` missing."},"404":{"description":"No such subscription, or the token does not match it."},"503":{"description":"Unsubscribe is not configured on this deployment."}}}},"/api/embed/stack.json":{"get":{"tags":["sync"],"operationId":"getEmbedStack","summary":"The listing slice behind the embeddable stack widget","description":"What /embed/stack.js renders on a third-party page: up to ten scored listings for a use case and/or chain, with score, tier, liveness and chains. CORS-open and signed (Ed25519, key at /.well-known/jwks.json) because it is rendered on domains we do not control. Ordered by Sato Score — a measure of how open, active and verifiable a project is, never a safety, quality or returns grade, and no winner is declared. CC-BY-4.0; the attribution link is part of the widget.","parameters":[{"name":"use_case","in":"query","required":false,"schema":{"type":"string"},"description":"A Sato Hub use-case slug. An unrecognised value is ignored rather than erroring."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"A chain name as the directory spells it, e.g. Base or Solana."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":10,"default":5}}],"responses":{"200":{"description":"The slice, with the data's own as-of date.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"as_of":{"type":"string","format":"date","nullable":true,"description":"The newest check date across the listings shown — never the render date."},"count":{"type":"integer"},"items":{"type":"array","items":{"type":"object"}},"attribution":{"type":"object"},"caveat":{"type":"string"}}}}}}}}},"/api/registry/search":{"get":{"tags":["registry"],"operationId":"searchAgents","summary":"Search listed Agent Passports","description":"Registered onchain agents. Registration proves a claim was made, and a verified wallet proves control of a key — neither is an endorsement of the agent.","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":200},"description":"Free text over name, description, creator, stack, and type."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"}},{"name":"type","in":"query","required":false,"schema":{"type":"string"},"description":"Agent utility type, substring-matched."}],"responses":{"200":{"description":"Matching agents.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"agents":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/resolve":{"get":{"tags":["resolve"],"operationId":"resolveListing","summary":"Resolve a GitHub repository or npm package to its Sato Hub listing","description":"Pass exactly one of `repo` (a GitHub URL or owner/name) or `package` (an npm package name). Matching is exact: the listing's GitHub owner/name, or a package that a listing's deploy spec installs. Nothing fuzzy — a wrong hit would put the wrong score on somebody's README. `sato_score` is null when the listing carries no score (editorial categories, or not yet assessed). The score is a 0-100 measure of how open, active and verifiable the project is; it is not a safety, quality or returns grade.","parameters":[{"name":"repo","in":"query","required":false,"schema":{"type":"string"},"description":"GitHub repository URL (https://github.com/owner/name, .git and paths tolerated) or bare owner/name."},{"name":"package","in":"query","required":false,"schema":{"type":"string"},"description":"npm package name, scoped allowed; a trailing @version is ignored."}],"responses":{"200":{"description":"The matching listing.","content":{"application/json":{"schema":{"type":"object","required":["slug","name","sato_score","verify_url","sato_url","matched_by"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"sato_score":{"type":"integer","nullable":true,"minimum":0,"maximum":100},"sato_tier":{"type":"string","nullable":true,"description":"High | Medium | Low, or null when unscored."},"verify_url":{"type":"string","format":"uri","nullable":true,"description":"The public Sato Score report."},"sato_url":{"type":"string","format":"uri","description":"Canonical listing page — cite this."},"badge_url":{"type":"string","format":"uri"},"badge_json_url":{"type":"string","format":"uri"},"matched_by":{"type":"string","enum":["repo","package"]},"matched_value":{"type":"string"},"caveat":{"type":"string"},"meta":{"type":"object","description":"rules / coverage / next_update — see /api/export/adoption.json."}}}}}},"400":{"description":"Neither parameter given, or the value is not a GitHub URL / npm name."},"404":{"description":"No listing matches. The body includes `submit_url` for adding one."},"410":{"description":"A listing matched but was RETIRED (status Deprecated — no public activity over 90 days). Body carries slug, name, status and sato_url; sato_score is null because a retired project's last score is not a current fact."}}}},"/api/check":{"get":{"tags":["check"],"operationId":"checkCustody","summary":"Read what a package, MCP server, skill, repo or x402 endpoint can do with your keys and funds","description":"Returns a sato.custody/v1 profile with four answers: does it take your key, does your key leave, can it move funds on its own, what changed. Each evidence line carries one class — declared (the project says so), traced (our static read of the published artifact) or observed (a sandbox run with planted test keys). A stored profile answers first; an unstored subject gets the declared and traced lanes live, capped at 10 s; the observed lane never runs from a request. Signed like /api/preflight. A profile is not a safety rating, an audit or an endorsement, and `unknown` means we could not look.","parameters":[{"name":"target","in":"query","required":true,"schema":{"type":"string"},"description":"npm:<name>, a bare package name, pypi:<name>, a github.com URL, an https MCP endpoint, a ClawHub/skills.sh URL, or an x402 resource URL."},{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["package","mcp","skill","repo","x402"]},"description":"Disambiguates the target, e.g. kind=x402 for a paid URL."}],"responses":{"200":{"description":"The profile, its summary and the four answers. Schema: https://satohub.ai/schemas/custody/v1.json","content":{"application/json":{"schema":{"type":"object","required":["schema","profile","summary","answers","check_url"]}}}},"400":{"description":"The target could not be read; the body says why in one sentence."},"503":{"description":"The custody engine is not switched on for this deployment."},"504":{"description":"The live lanes ran past their 10-second budget."}}}},"/api/check/install":{"post":{"tags":["check"],"operationId":"checkInstall","summary":"Check every target in an install command or MCP config before installing it","description":"Parses npm/pnpm/yarn/bun add|install, npx -y, pip install, uvx, `claude mcp add …` and {\"mcpServers\": …} JSON, and returns one custody summary with the four answers per target. Targets that could not be profiled are listed in `unresolved` with the reason. `has_observed_key_egress` is true only when a planted test key was seen leaving. Body cap 16 KB; rate limited per caller. Nothing from the body is recorded.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"command":{"type":"string","description":"A shell install command."},"config":{"type":"string","description":"An MCP config block (JSON as text)."}}}}}},"responses":{"200":{"description":"One entry per target.","content":{"application/json":{"schema":{"type":"object","required":["schema","subjects","unresolved","has_observed_key_egress"]}}}},"400":{"description":"Neither command nor config was given, or the body was not JSON."},"413":{"description":"The body is larger than 16 KB."},"429":{"description":"Too many requests from this address."},"503":{"description":"The custody engine is not switched on for this deployment."}}}},"/api/check/batch":{"post":{"tags":["check"],"operationId":"checkBatch","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Sato Check a list of targets or an install block against an org custody policy","description":"Bulk custody check for platforms and CI. Takes `targets` (strings, as GET /api/check takes them) and/or `install` (install commands or an MCP config block), plus an optional `policy` — the .sato/policy.json shape `{version:1, fail_on:[…], allow_hosts?, allow_subjects?}`; with no policy, only `key_egress_observed` matches. Stored profiles answer first; unstored subjects are profiled live (declared + traced) up to the caller's live budget, and anything past it is listed in `unresolved` WITH the reason. Target cap and live budget follow the API-key tiers (free 50/10; more with a key via `Authorization: Bearer sk_sato_…` or `X-Sato-Key`); over the cap is refused, never truncated. `exit_code` is 1 only when a policy rule matched a reading; `unknown` never matches a rule. `?format=sarif` returns a SARIF 2.1.0 log. A match names the rule and its evidence rule id; it is a description of what was read, not a safety rating.","parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","sarif"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"targets":{"type":"array","items":{"type":"string"}},"install":{"type":"string","description":"Install commands or an MCP config block."},"policy":{"type":"object","properties":{"version":{"type":"integer","enum":[1]},"fail_on":{"type":"array","items":{"type":"string","enum":["key_egress_observed","undeclared_key_read","unlimited_fund_action","install_script_added","new_host"]}},"allow_hosts":{"type":"array","items":{"type":"string"}},"allow_subjects":{"type":"array","items":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Per-subject summary, answers and policy result; or a SARIF log with ?format=sarif.","content":{"application/json":{"schema":{"type":"object","required":["schema","policy","summary","exit_code","results","unresolved"]}}}},"400":{"description":"Body not JSON, invalid policy, no targets, or over the target cap."},"413":{"description":"The body is larger than 256 KB."},"429":{"description":"Daily limit for this key's tier reached."}}}},"/api/preflight":{"get":{"tags":["preflight"],"operationId":"preflightTarget","summary":"Check a repo, package, endpoint, ERC-8004 agent or ERC-20 token before you install, connect, pay or trade","description":"Pass exactly one of `repo`, `package`, `endpoint`, `agent`, `token` (with `chain`) or `skill`. Returns `verdict` (go | caution | no | unknown) with one evidence line per check, each naming the field it was read from and when that field was written; `rule` cites the rule that decided it (the rules are written down in lib/preflight.ts). WHAT A VERDICT IS NOT: it is not a security review, a quality judgment, or a statement about returns, and `unknown` means Sato Hub holds no record of the target — not that anything is wrong with it. The `endpoint` lane runs ONE live MCP handshake (initialize + tools/list, 8 s cap) against an endpoint that is not in the directory, and can never answer `go`: a handshake is not a record. The `agent` lane confirms the ERC-8004 registration exists, fetches the registration file, and reports the services it DECLARES; only a declared MCP service is probed — A2A and x402 declarations are reported as declared and unprobed. The `token` lane reads a chain keylessly: bytecode presence and size, the ERC-20 views, the Clanker v4 factory's own deployment record, and the Uniswap v3 factory across the four standard fee tiers against wrapped native. It also returns a `token` object in which EVERY FIELD IS NULLABLE and each null carries the reason it is null. Permanently null, and named in the evidence rather than omitted: holder concentration (no keyless public source; explorers are not scraped) and Uniswap v4 / non-Uniswap liquidity (a v4 poolId cannot be reconstructed without the PoolKey). The deployer address requires an optional explorer key. A pool existing is not depth, a locker holds a position on the terms its own code enforces, and nothing in this lane asserts that a token is safe or audited. The `skill` lane reads the skills axis: the static disclosure of what a skill's own text declares and does — hosts it names, key generation, credential requests, remote-to-shell pipes, tool grants — with the LINES that produced each finding, plus installs, when it was last seen in its registry, and whether a host it names belongs to a listed project. It returns a `skill` object alongside the verdict. A DISCLOSURE DESCRIBES: it never says safe and it never says malicious, an empty flag list is reported as \"nothing matched\" rather than as a pass, and the hosting registry's own scan result is attributed to that registry by name rather than restated as ours. The scan is static, so what a skill does when it runs is outside what this lane can see.","parameters":[{"name":"repo","in":"query","required":false,"schema":{"type":"string"},"description":"GitHub repository URL or bare owner/name."},{"name":"package","in":"query","required":false,"schema":{"type":"string"},"description":"Package name; a trailing @version is ignored."},{"name":"endpoint","in":"query","required":false,"schema":{"type":"string","format":"uri"},"description":"An https MCP endpoint URL."},{"name":"agent","in":"query","required":false,"schema":{"type":"string"},"description":"An ERC-8004 reference, <chain>:<id>, e.g. base:42. Only chains hosting the singleton Identity Registry."},{"name":"token","in":"query","required":false,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"description":"An ERC-20 token contract address. Requires `chain`. EVM only in v1."},{"name":"chain","in":"query","required":false,"schema":{"type":"string","enum":["Base","Ethereum","Arbitrum","Robinhood Chain"]},"description":"Chain for `token`. Any other chain, Solana included, answers verdict `unknown` with the reason — never a guess."},{"name":"skill","in":"query","required":false,"schema":{"type":"string"},"description":"An agent skill: `<registry>/<id>` (clawhub, skills.sh, github), or the skill id alone when it is unique. Reads the static disclosure the weekly sweep already produced; nothing is fetched from the registry at request time and no skill is ever executed."},{"name":"x402","in":"query","required":false,"schema":{"type":"string","format":"uri"},"description":"An https x402 resource URL. ONE unpaid request (GET, POST on 405) reads the 402 payment terms — v1 body or v2 PAYMENT-REQUIRED header. No payment is sent or signed. Rules X1–X5 describe the payee; they are never a reputation score."}],"responses":{"200":{"description":"The verdict and its evidence.","content":{"application/json":{"schema":{"type":"object","required":["verdict","evidence","target","checked_at","caveat"],"properties":{"verdict":{"type":"string","enum":["go","caution","no","unknown"],"description":"unknown is a value, not an error: we hold no record."},"rule":{"type":"string","description":"The rule that decided it, e.g. R5. Rules are documented in the file header of lib/preflight.ts."},"target":{"type":"object","properties":{"kind":{"type":"string","enum":["repo","package","endpoint","agent","token","skill","x402"]},"value":{"type":"string","description":"The normalised identifier."},"slug":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"sato_url":{"type":"string","format":"uri","nullable":true,"description":"Canonical listing page — cite this."},"verify_url":{"type":"string","format":"uri","nullable":true}}},"evidence":{"type":"array","description":"One line per check. Every line names the field it was read from, so the verdict can be re-derived rather than trusted.","items":{"type":"object","required":["check","result","source_field","checked_at"],"properties":{"check":{"type":"string"},"result":{"type":"string","description":"A reading, never a judgment."},"source_field":{"type":"string","description":"e.g. resources.uptime_observed.success_rate_pct, probe.mcp_probe, erc8004.identity_registry.tokenURI."},"checked_at":{"type":"string","format":"date-time","nullable":true,"description":"Null when the field carries no date."}}}},"checked_at":{"type":"string","format":"date-time"},"caveat":{"type":"string"},"meta":{"type":"object","description":"rules / coverage / next_update — see /api/export/adoption.json."}}}}}},"400":{"description":"No target, more than one target, or a value that is not a repo / package / https URL / <chain>:<id>."},"410":{"description":"The target resolves to a RETIRED listing (status Deprecated). The body is a normal verdict payload with verdict `no`."}}}},"/api/route/agent":{"get":{"operationId":"routeAgent","summary":"Choose a registered agent for a task, with the pick explained","description":"Returns the registered agent Sato Hub would route a task to, with `chosen_by` — every signal, its value and the exact field it was read from — at `checked_at`. Candidates are the listed Sato Agent Passports; the top 3 by the static ranking get one live MCP handshake first. An ERC-8004 registration proves a claim was made on-chain, not that the agent works; a probe proves the declared service answered once; a null feedback count is unknown, never zero. Agents registered on-chain but holding no Sato passport are absent from the pool — absent means unseen, not unqualified. This endpoint never signs, relays or pays. 404 `{unknown, reason}` means nothing qualified; 503 `{unavailable}` means the registry could not be read.","tags":["route"],"parameters":[{"name":"capability","in":"query","required":false,"description":"What the work needs, matched against the candidate's name, declared agent types and declared services.","schema":{"type":"string","maxLength":120}},{"name":"chain","in":"query","required":false,"description":"Chain the agent must claim, e.g. base.","schema":{"type":"string","maxLength":40}},{"name":"requires_mcp","in":"query","required":false,"description":"Only candidates that DECLARE an MCP service. A declaration is not a working service.","schema":{"type":"boolean"}},{"name":"requires_x402","in":"query","required":false,"description":"Only candidates that DECLARE x402 payment support.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"A route, with the readings that produced it.","content":{"application/json":{"schema":{"type":"object","required":["route","coverage","rules","caveat","checked_at"],"properties":{"route":{"type":"object","required":["agent","chosen_by","checked_at","alternatives","caveat"],"properties":{"agent":{"type":"object","properties":{"id":{"type":"string"},"source":{"type":"string","enum":["erc8004","passport"]},"name":{"type":"string"},"declared_services":{"type":"object"},"registration_confirmed":{"type":"boolean"},"service_answering":{"type":"boolean","nullable":true},"feedback_count":{"type":"integer","nullable":true},"last_seen":{"type":"string","nullable":true},"wallet_verified":{"type":"boolean"}}},"chosen_by":{"type":"array","items":{"type":"object","required":["signal","value","source_field"],"properties":{"signal":{"type":"string"},"value":{"type":"string"},"source_field":{"type":"string"}}}},"alternatives":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"behind_on":{"type":"string"}}}},"checked_at":{"type":"string"},"caveat":{"type":"string"}}},"preflight":{"type":"object","nullable":true,"properties":{"verdict":{"type":"string","enum":["go","caution","no","unknown"]},"rule":{"type":"string"},"caveat":{"type":"string"}}},"candidates_considered":{"type":"integer"},"probed":{"type":"integer"},"coverage":{"type":"object","properties":{"sources":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}},"rules":{"type":"array","items":{"type":"string"}},"caveat":{"type":"string"},"checked_at":{"type":"string"}}}}}},"404":{"description":"Nothing qualified. `{ unknown: true, reason, coverage, rules, caveat, checked_at }` — an answer, not an error."},"503":{"description":"The agent registry could not be read. No route is offered rather than one chosen on data we could not read."}}}},"/api/route/x402":{"get":{"operationId":"routeX402","summary":"Choose a verified x402 endpoint for a capability","description":"Returns the declared x402 endpoint Sato Hub would route a request to for a capability, with `chosen_by` — every signal, its value, the field it came from and the date. A verified 402 is a protocol observation (on that date the URL asked for money in a payload a client could parse), not a delivery, solvency or safety claim, and no payment was sent to establish it. This endpoint does not relay, pay, sign or settle: the buyer calls the endpoint and settles the seller's own 402 with their own wallet. 404 `{unknown}` means no standing verification for that query; 503 `{unavailable}` means the measurement record could not be read.","tags":["route"],"parameters":[{"name":"capability","in":"query","required":false,"description":"Capability classification, from the listing's category/tags and the published endpoint path.","schema":{"type":"string","enum":["price-feed","onchain-data","swap-quote","inference","storage","search","identity","other"]}},{"name":"chain","in":"query","required":false,"description":"Settlement network the 402 payload must declare, e.g. base, base-sepolia, solana, eip155:8453.","schema":{"type":"string"}}],"responses":{"200":{"description":"A route, with the readings that produced it.","content":{"application/json":{"schema":{"type":"object","required":["endpoint","host","chosen_by","terms","caveat","checked_at"],"properties":{"endpoint":{"type":"string","format":"uri"},"host":{"type":"string"},"slug":{"type":"string","nullable":true},"capability":{"type":"string","nullable":true},"chain":{"type":"string","nullable":true},"capabilities":{"type":"array","items":{"type":"string"}},"terms":{"type":"object","properties":{"scheme":{"type":"string","nullable":true},"network":{"type":"string","nullable":true},"asset":{"type":"string","nullable":true},"price":{"type":"string"}}},"chosen_by":{"type":"array","items":{"type":"object","required":["signal","value","source_field","checked_at"],"properties":{"signal":{"type":"string"},"value":{"type":"string"},"source_field":{"type":"string"},"checked_at":{"type":"string","nullable":true}}}},"preflight":{"type":"object","properties":{"verdict":{"type":"string","enum":["go","caution","no","unknown"]},"rule":{"type":"string"},"caveat":{"type":"string"}}},"alternatives":{"type":"array","items":{"type":"object"}},"alternatives_total":{"type":"integer"},"caveat":{"type":"string"},"checked_at":{"type":"string","nullable":true}}}}}},"400":{"description":"Unknown capability. The body lists the ones we classify."},"404":{"description":"No endpoint holds a standing verification for this capability/chain. `{ unknown: true, reason }` — an answer, not an error."},"503":{"description":"The x402 verification record could not be read. `{ unavailable: true, reason }` — no route is offered rather than one chosen on data we could not read."}}}},"/api/swap/quote":{"post":{"tags":["swap"],"operationId":"swapQuote","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Quote a swap, check it against a policy, and return an unsigned transaction","description":"The Sato Swap call. Chooses the venue the way /api/route/swap does, then CHECKS the trade before anything is built.\n\nTWO MODES. `recommend` (default) returns the choice, the fee sentence, the verdicts and the policy in force, and NEVER a transaction — whatever the gate said. `build-tx` returns an UNSIGNED transaction, and only when the gate allowed it AND a simulation did not revert.\n\nWHAT IS CHECKED: four targets — `token_in`, `token_out`, the venue endpoint and the recipient — each with its own verdict (go | caution | no | unknown), the rule id that produced it and the reason. Then the caller’s policy: caps per trade and per period, allowed chains, tokens, venues and recipients, maximum slippage and deadline. Manage policies at /api/swap/policies; with no stored policy the default applies, and the default refuses on unknown.\n\nUNKNOWN REFUSES BY DEFAULT. A target that could not be read, or a simulation that could not run, withholds the transaction unless the policy says otherwise, and `withheld.reason` names the lane that could not be read.\n\nA REFUSAL IS A 200. Gate refusals, policy limits and unsimulatable transactions all come back as 200 with `tx: null` and `withheld`. 4xx is reserved for a body we could not read, and for auth and rate limits.\n\nCOUNTERPARTY: `no_record` means we hold no Agent Passport for the recipient. That is the ordinary case and is not a finding against it; `found` is not an endorsement, since a Passport is self-registered and `wallet_verified` proves control of a key rather than anything about the product.\n\nTHE FEE is the venue’s own sentence, quoted verbatim in `disclosure`, before anything is signed. A trade that is never signed pays nothing.\n\nCUSTODY: none. This endpoint holds no keys, signs nothing and broadcasts nothing.\n\nThe response is Ed25519-signed (`meta.signature`, `Sato-Signature` header) when a signing key is configured, and carries `Link: <https://satohub.ai/schemas/swap-quote/v1.json>; rel=\"describedby\"`. Responses are `no-store`: a cached quote is a stale price.\n\nRATE LIMITS: anonymous callers are bounded per hour per caller — 30 `recommend`, 10 `build-tx`. A Sato API key replaces that with the tier’s attributed daily budget plus a per-hour `build-tx` allowance; a key only ever raises a limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chain_in","token_in","token_out","amount_in"],"properties":{"chain_in":{"type":"string","maxLength":40,"description":"Source chain as the directory writes it, e.g. Base, Ethereum, Solana."},"chain_out":{"type":"string","maxLength":40,"description":"Destination chain. Omit, or repeat chain_in, for a same-chain swap. A different value is the cross-chain lane; the two lanes are never compared with each other."},"token_in":{"type":"string","maxLength":120,"description":"Contract address, Solana mint, or a symbol for the well-known stablecoins."},"token_out":{"type":"string","maxLength":120},"amount_in":{"type":"string","pattern":"^[0-9]{1,78}$","description":"Base units of the INPUT token, as a string — a uint256 does not survive a JSON number."},"recipient":{"type":"string","nullable":true,"maxLength":120,"description":"Where the output goes. Omit to send to the taker."},"taker":{"type":"string","nullable":true,"maxLength":120,"description":"The address that would sign. Some venues only return a transaction when it is given; nothing is signed here."},"slippage_bps":{"type":"integer","minimum":0,"maximum":5000,"default":50},"deadline":{"type":"integer","nullable":true,"description":"Unix seconds the quote should stay good until. Omit for the venue default."},"usd_notional":{"type":"number","nullable":true,"description":"USD notional of amount_in when the CALLER already holds a price. Omitted is unknown, never zero: a USD cap does not bite without it."},"mode":{"type":"string","enum":["recommend","build-tx"],"default":"recommend"},"sato_agent_id":{"type":"string","description":"An Agent Passport to act as. Honoured ONLY when a signed-in session proves ownership of it; an unproven id is dropped rather than recorded."}}}}}},"responses":{"200":{"description":"The quote, the verdicts, the simulation and either an unsigned transaction or the reason one was withheld. Also the shape a gate or policy refusal arrives in.\n\nNO ROUTE is the one 200 that is not a quote: when no adapter returned a quote for the pair (or none serves the chain), the body is `{ unavailable, tried, checked_at, caveat }` — the reason, and every venue asked with what it said. It carries NO `Link: rel=\"describedby\"`, because it is not a swap-quote; tell the two apart by the presence of `unavailable`.","headers":{"Link":{"description":"rel=\"describedby\" pointing at /schemas/swap-quote/v1.json. Present on a quote only — absent on the no-route answer and on 4xx.","schema":{"type":"string"}},"Sato-Signature":{"description":"Ed25519 over the exact response bytes, when a signing key is configured.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/swap-quote"},{"type":"object","title":"No route","required":["unavailable","tried","checked_at"],"properties":{"unavailable":{"type":"string","description":"Why no route is offered. A reading of this moment, not a statement that the pair cannot be traded."},"tried":{"type":"array","description":"Every adapter that was asked and what it said. Empty when no adapter serves the chain.","items":{"type":"object","properties":{"venue":{"type":"string"},"reason":{"type":"string"},"source_url":{"type":"string"}}}},"checked_at":{"type":"string","format":"date-time"},"caveat":{"type":"string"}}}]}}}},"400":{"description":"The body did not parse, a field was out of range, or token_in equalled token_out."},"429":{"description":"Over the anonymous burst limit for this mode, or over the key’s daily budget."}}}},"/api/swap/receipts/{id}":{"get":{"tags":["swap"],"operationId":"getSwapReceipt","summary":"The public record of one swap decision","description":"What was checked for one `route_id`, under which policy, at what instant: the quote, the mode and lane, the gate verdicts with their digest, the simulation digest, and whether the recipient held an Agent Passport.\n\nWHAT IT IS NOT: a claim the trade filled, a claim anyone was paid, or a safety verdict. A refusal recorded here is a statement about what we could read. ONE ROW IS ONE DECISION — rows are never summed into volume, fills or a fee total.\n\nThe payload is an allowlist, not a delete list: a column added later stays invisible until someone decides it should be public. It never carries an IP hash, the row id, or the raw props bag. A receipt marked non-public answers 404, the same as an unknown id.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^rt_[a-z2-9]{12}$"}}],"responses":{"200":{"description":"The public receipt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/swap-receipt"}}}},"404":{"description":"Unknown id, a receipt that is not public, or no receipt store configured."}}}},"/api/swap/policies":{"get":{"tags":["swap"],"operationId":"listSwapPolicies","security":[{"bearerAuth":[]},{"satoKey":[]}],"summary":"The swap policies bound to the calling subject","description":"A policy decides what a caller may swap: allowed chains, tokens, venues and recipients, caps per trade and per period, maximum slippage and deadline, and what happens on a `caution` or `unknown` verdict.\n\nFAILS CLOSED. There is no anonymous policy — a policy you cannot attribute is a policy someone else can widen — so an unidentified caller gets 401 and a plain statement that the DEFAULT policy applies, which refuses on unknown. THE SUBJECT COMES FROM THE AUTH, never from the body: a caller cannot write a policy for another key, another Passport or another wallet.","responses":{"200":{"description":"The caller’s live policies, and the default when there are none."},"401":{"description":"No identified subject. The default policy applies."},"503":{"description":"No policy store configured on this deployment."}}},"post":{"tags":["swap"],"operationId":"putSwapPolicy","security":[{"bearerAuth":[]},{"satoKey":[]}],"summary":"Store a swap policy (a new version; the previous one is revoked)","description":"Each write stores a NEW VERSION as a new row and revokes the previous one in the same breath, so a receipt citing (policy_id, version) always points at stored bytes. Revocation is a timestamp, never a delete. The subject is set from the auth and the three subject columns in the body are overwritten before validation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"The stored policy."},"400":{"description":"The policy did not validate, or it named no subject or more than one."},"401":{"description":"No identified subject."},"429":{"description":"Too many policy writes for this subject."},"503":{"description":"No policy store configured on this deployment."}}},"delete":{"tags":["swap"],"operationId":"revokeSwapPolicy","security":[{"bearerAuth":[]},{"satoKey":[]}],"summary":"Revoke one policy","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked."},"401":{"description":"No identified subject."},"404":{"description":"No live policy with that id for this subject."}}}},"/api/route/swap":{"get":{"tags":["route"],"operationId":"routeSwap","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Choose a venue for a swap and return its quote with the fee disclosed","description":"Asks every aggregator adapter that quotes on the chain, in parallel, and returns the chosen venue's quote and calldata.\n\nWHAT THE CHOICE IS: a recommendation. The venue is ranked on liveness (`last_activity_at`), the observed record of Sato Hub's OWN daily checks (`uptime_observed` — the share of our checks that succeeded, NEVER uptime, since a failure can be on our side), verification state (a reproduced install, a reviewed listing), and then quoted price. Retired and dormant venues are excluded outright. `chosen_by` names each signal with the field it was read from, and `checked_at` says when — so the route can be re-derived rather than trusted. Nothing in the response is called best, safe or guaranteed, and a quote is not a fill: price, liquidity and gas move between the quote and the signature.\n\nTHE FEE: 3 bps when both legs are stablecoins, 15 bps when either leg is volatile. It is a parameter on the aggregator's own quote (0x `swapFeeBps`/`swapFeeRecipient`, 1inch `fee`/`referrer`, Jupiter `platformFeeBps` into a referral account), taken inside the swap transaction by that router, and stated in `disclosure` on every response BEFORE anything is signed. A failed, reverted or unsigned trade pays nothing. `sato_fee_bps` is null when a venue configures the rate server-side against a registered code — we do not print a rate we did not send.\n\nCUSTODY: none. This endpoint never signs, holds, moves or broadcasts funds. It returns calldata the caller may sign.\n\nResponses are `no-store`: a cached route is a stale price.","parameters":[{"name":"chain","in":"query","required":true,"schema":{"type":"string"},"description":"Chain display name as the directory writes it, e.g. Base, Ethereum, Solana."},{"name":"token_in","in":"query","required":true,"schema":{"type":"string"},"description":"Input token address (or Solana mint), or a symbol for the well-known stablecoins."},{"name":"token_out","in":"query","required":true,"schema":{"type":"string"},"description":"Output token address (or Solana mint), or a symbol."},{"name":"amount","in":"query","required":true,"schema":{"type":"string"},"description":"Sell amount in the INPUT token's base units, e.g. 1000000 for 1 USDC."},{"name":"slippage_bps","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":5000},"description":"Slippage tolerance in basis points, passed through to the venue."},{"name":"taker","in":"query","required":false,"schema":{"type":"string"},"description":"The address that would sign. Some venues only return calldata when it is given. Nothing is signed here."}],"responses":{"200":{"description":"The chosen venue, its quote, the fee and the reasons.","content":{"application/json":{"schema":{"type":"object","required":["route_id","route","quote","sato_fee_bps","disclosure","chosen_by","checked_at","caveat"],"properties":{"route_id":{"type":"string","description":"Identifies THIS quote, e.g. rt_ab3k9mq2xrtv. Report what became of it within 24 hours via POST /api/route/receipt. Random and short-lived; it identifies a quote, not a caller."},"route":{"type":"object","description":"The chosen venue. `listed: false` means it answered with a quote but has no Sato Hub listing — unknown, not bad.","properties":{"slug":{"type":"string"},"name":{"type":"string"},"listed":{"type":"boolean"},"sato_url":{"type":"string","format":"uri","nullable":true},"liveness":{"type":"string","nullable":true,"enum":["Active","Recent","Quiet",null]},"observed_success_pct":{"type":"number","nullable":true,"description":"Share of OUR daily checks that succeeded. Never uptime. Null below 14 days observed."},"install_verified":{"type":"boolean"}}},"quote":{"type":"object","properties":{"venue":{"type":"string"},"amount_in":{"type":"string"},"amount_out":{"type":"string","description":"Base units of the output token, as quoted by the venue's own API at checked_at."},"token_in":{"type":"string"},"token_out":{"type":"string"},"chain":{"type":"string"},"calldata":{"type":"string","nullable":true,"description":"The transaction data the caller may sign. Sato Hub never signs it."},"tx":{"type":"object","nullable":true},"source_url":{"type":"string","format":"uri","description":"The venue API doc the request was shaped from."}}},"sato_fee_bps":{"type":"integer","nullable":true,"description":"3 stable-to-stable, 15 with any volatile leg; null when the venue sets the rate against a registered referral code."},"sato_fee_recipient":{"type":"string","nullable":true},"disclosure":{"type":"string","description":"Stated before anything is signed. Not optional and not a footnote."},"chosen_by":{"type":"array","description":"One entry per signal the choice was made on, each naming its field.","items":{"type":"object","required":["signal","value","source_field"],"properties":{"signal":{"type":"string"},"value":{"type":"string"},"source_field":{"type":"string"}}}},"checked_at":{"type":"string","format":"date-time"},"alternatives":{"type":"array","items":{"type":"object"},"description":"Every other candidate venue with its own numbers, in rank order."},"caveat":{"type":"string"},"preflight":{"type":"object","nullable":true,"description":"A Preflight verdict for the chosen venue's listing — see the preflight tag. Not a safety statement."},"unavailable_venues":{"type":"array","items":{"type":"object"},"description":"Adapters that were asked and did not answer, with the reason."}}}}}},"400":{"description":"Missing or invalid chain / token_in / token_out / amount / slippage_bps."},"503":{"description":"No adapter returned a quote. The body carries `unavailable` and `tried` — every adapter asked and what it said. A reading of this moment, not a statement that the pair cannot be traded."}}}},"/api/route/lp":{"get":{"tags":["route"],"operationId":"routeLp","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Compare the Uniswap pools for a pair, with the evidence and the missing term named","description":"Every Uniswap v3 pool observed for a pair across the four standard fee tiers, with the one Sato Route names as chosen.\n\nWHAT IS RETURNED PER POOL: the pool address, the fee tier as read from the pool itself, in-range liquidity, the number of swaps and the volume actually counted, the window that count covered (`covered_days` — measured, never the window we asked for), and a FEE-REVENUE PROXY.\n\nWHAT A PROXY IS: the pool's published fee rate multiplied by the swap volume we counted, in token0's own units. It is NOT revenue, NOT an APR and NOT a yield, and it is never scaled up from the covered window to a full week — the log walk is capped, so two tiers of the same pair are routinely read over different spans and only the per-day form is comparable.\n\nTHE LARGEST TERM IS ABSENT BY DESIGN: impermanent loss is not modelled, not approximated and not bounded. It appears in `chosen_by` as an explicit null so it cannot be mistaken for something that was accounted for. This endpoint is not advice, and no pool is called safe, best or profitable.\n\nTHE PICK: the highest observed fee-revenue proxy per day per unit of in-range liquidity, among the pools whose window could be read. When nothing is rankable, `chosen` is null with a per-pool reason rather than a low-confidence pick.\n\nCOVERAGE: Uniswap v3 on Base and Ethereum, for the pairs Sato Route quotes. Uniswap v4 rows are returned with `fee_revenue_proxy: null` and their reason — v4 is a singleton PoolManager and publishes no per-pair pool id, so unknown is the honest reading and is never rendered as zero. Every non-Uniswap venue is outside this endpoint entirely.\n\nCUSTODY: none. Nothing here signs, holds, moves or provides liquidity.\n\nReadings are banked weekly by the collector, so responses are cacheable.","parameters":[{"name":"chain","in":"query","required":true,"schema":{"type":"string","enum":["Base","Ethereum"]},"description":"Chain the pool is on, any casing. A chain the lane does not collect answers 404 with the covered list — unknown, never an empty result presented as none."},{"name":"pair","in":"query","required":true,"schema":{"type":"string"},"description":"The pair, either way round and with -, / or _ between: WETH-USDC, usdc/weth, WBTC_WETH."}],"responses":{"200":{"description":"The pools observed for the pair, and the one chosen.","content":{"application/json":{"schema":{"type":"object","required":["chain","pair","pools","chosen","checked_at","caveat","rules"],"properties":{"chain":{"type":"string"},"pair":{"type":"string","description":"Canonical form, e.g. weth-usdc."},"week":{"type":"string","nullable":true,"description":"The ISO week the readings were banked in."},"pools":{"type":"array","items":{"type":"object","required":["venue","unit","method","evidence_tier","as_of"],"properties":{"venue":{"type":"string","enum":["uniswap-v3","uniswap-v4"]},"pool":{"type":"string","nullable":true},"fee_tier_ppm":{"type":"integer","nullable":true,"description":"Read from the pool's own fee(), in parts per million."},"fee_tier_pct":{"type":"number","nullable":true},"liquidity":{"type":"string","nullable":true,"description":"In-range L as a decimal string. Null = not read, never 0."},"swaps_observed":{"type":"integer","nullable":true},"volume_token0_observed":{"type":"number","nullable":true,"description":"Σ|amount0| over the covered window, in token0 units."},"fee_revenue_proxy":{"type":"number","nullable":true,"description":"fee rate × observed volume, in token0 units. A PROXY, never revenue. Null = the window could not be read."},"fee_revenue_proxy_per_day":{"type":"number","nullable":true,"description":"The proxy divided by covered_days — the only form comparable across tiers."},"proxy_per_day_per_liquidity":{"type":"number","nullable":true,"description":"proxy_per_day ÷ in-range liquidity. A ranking ratio for tiers of ONE pair, not a yield."},"token0":{"type":"string","nullable":true},"token1":{"type":"string","nullable":true},"unit":{"type":"string"},"covered_days":{"type":"number","nullable":true,"description":"The span we actually read. Not the span we asked for."},"window_days":{"type":"integer","nullable":true},"window_complete":{"type":"boolean","description":"False means the log walk was capped and covered_days is the real window."},"method":{"type":"string","description":"How the number was produced, in words."},"evidence_tier":{"type":"integer"},"caveat":{"type":"string","nullable":true},"as_of":{"type":"string","format":"date-time"},"unknown_reason":{"type":"string","nullable":true,"description":"Why this pool has no proxy. Present exactly when fee_revenue_proxy is null."}}}},"chosen":{"type":"object","nullable":true,"description":"Null when nothing was rankable — never a low-confidence pick.","properties":{"pool":{"type":"string","nullable":true},"venue":{"type":"string"},"fee_tier_ppm":{"type":"integer","nullable":true},"because":{"type":"string","description":"\"chosen by Sato Route on <date>, because …\", ending with the reminder that impermanent loss is not modelled."},"chosen_by":{"type":"array","description":"Every signal the pick was made on, with the field it was read from. Includes impermanent loss with a null value, because its absence is part of the answer.","items":{"type":"object","required":["signal","value","source_field"],"properties":{"signal":{"type":"string"},"value":{"nullable":true},"source_field":{"type":"string"}}}}}},"unranked":{"type":"array","items":{"type":"object","properties":{"pool":{"type":"string","nullable":true},"fee_tier_ppm":{"type":"integer","nullable":true},"reason":{"type":"string"}}}},"as_of":{"type":"string","format":"date-time","nullable":true},"checked_at":{"type":"string","format":"date-time"},"caveat":{"type":"string"},"rules":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"chain and pair are both required."},"404":{"description":"The pair is not collected, or no reading has been banked for it yet. The body carries the covered list. Unknown, not zero."}}}},"/api/preflight/batch":{"post":{"tags":["preflight"],"operationId":"preflightBatch","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Preflight a whole dependency manifest in one call","description":"Run Preflight over up to 50 targets at once (more with an API key — see /pricing; send it as `Authorization: Bearer sk_sato_…` or `X-Sato-Key`, and note that the endpoint works with no key at all) — a raw `package.json` or `requirements.txt` under `manifest` (with `manifest_kind`), and/or explicit `packages` / `repos` / `endpoints` / `agents` / `skills` arrays. Built for a CI step: the response carries a `summary`, an `exit_code` a build can use directly, and `findings` in SARIF 2.1 `results` shape so annotations and code-scanning uploads work without a translation layer. Reference implementation: github.com/satohubai/preflight-action.\n\nEXIT CODE: non-zero only when something came back `no` (or `caution` when `fail_on` is set to `caution`). `unknown` NEVER contributes to a non-zero exit at any setting — it means Sato Hub holds no record of that target, which is a fact about our index rather than about the caller's dependency.\n\nA MANIFEST NEVER CAUSES A PROBE. Names parsed out of a manifest are looked up against records we already hold; nothing in a manifest is fetched, resolved or contacted. Only `endpoints` and `agents`, which the caller typed on purpose, reach the live lanes — and those are budgeted per request, with anything past the budget answered `unknown` WITH the reason rather than dropped.\n\nTHE CAP IS REFUSED, NOT TRUNCATED: over 50 targets answers 400. A clean summary computed over a silent subset of somebody's dependencies would be believed, which makes truncation the more dangerous behaviour.\n\nWHAT A VERDICT IS: a description of what was checked and when. It is not a security review, a vulnerability scan, a licence check or a quality judgment, and no string this endpoint emits says otherwise.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A manifest, explicit target arrays, or both. At least one target must result.","properties":{"manifest":{"type":"string","description":"The raw text of the manifest file, up to 512 KB."},"manifest_kind":{"type":"string","enum":["package.json","requirements.txt"],"description":"Required with `manifest` unless `manifest_path` names a file whose kind can be inferred."},"manifest_path":{"type":"string","description":"The path the manifest came from. Used only to label SARIF findings so annotations land on the right file."},"packages":{"type":"array","items":{"type":"string"},"description":"Package names, checked in addition to the manifest."},"repos":{"type":"array","items":{"type":"string"},"description":"GitHub repositories, owner/name or URL."},"endpoints":{"type":"array","items":{"type":"string","format":"uri"},"description":"https MCP endpoints. The ONLY input that can cause a live probe."},"agents":{"type":"array","items":{"type":"string"},"description":"ERC-8004 references, <chain>:<id>."},"skills":{"type":"array","items":{"type":"string"},"description":"Agent skills, <registry>/<id>. Looked up against records the weekly sweep already wrote — no registry is contacted and no skill is run."},"fail_on":{"type":"string","enum":["no","caution","none"],"default":"no","description":"Which verdict makes `exit_code` non-zero. `unknown` is not an option, because it must never fail a build."}}}}}},"responses":{"200":{"description":"One verdict per target, a summary, an exit-code suggestion and SARIF findings.","content":{"application/json":{"schema":{"type":"object","required":["summary","exit_code","results","findings","checked_at","caveat"],"properties":{"summary":{"type":"object","description":"Counts by verdict. The four always sum to `total`.","properties":{"go":{"type":"integer"},"caution":{"type":"integer"},"no":{"type":"integer"},"unknown":{"type":"integer"},"total":{"type":"integer"}}},"exit_code":{"type":"integer","enum":[0,1],"description":"What a CI step should exit with. `unknown` can never make this 1."},"fail_on":{"type":"string","enum":["no","caution","none"]},"results":{"type":"array","description":"One entry per target, in the order the targets were derived.","items":{"type":"object","properties":{"kind":{"type":"string","enum":["package","repo","endpoint","agent"]},"target":{"type":"string","description":"As the caller wrote it."},"verdict":{"type":"string","enum":["go","caution","no","unknown"]},"rule":{"type":"string","description":"The rule that decided it, e.g. R5. Documented at /preflight/methodology."},"evidence":{"type":"array","items":{"type":"object"},"description":"One line per check: what was checked, what came back, the field it was read from, and when that field was written."},"sato_url":{"type":"string","nullable":true},"slug":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"line":{"type":"integer","description":"1-based line in the manifest this target came from, when it came from one."},"reason":{"type":"string","description":"Present when the target was answered `unknown` without a lane being run — e.g. past the live-check budget."}}}},"findings":{"type":"array","description":"SARIF 2.1 `results` entries. `no` is level error, `caution` is warning, `go` and `unknown` are note. Wrap them in a SARIF log to upload to code scanning.","items":{"type":"object"}},"skipped":{"type":"array","items":{"type":"object"},"description":"Inputs that produced no target, each with the reason. Never silently dropped."},"manifest_path":{"type":"string","nullable":true},"checked_at":{"type":"string","format":"date-time"},"caveat":{"type":"string"}}}}}},"400":{"description":"Unparseable body, an unknown manifest kind, nothing to check, or more than 50 targets. The cap is refused rather than truncated."}}}},"/api/badge/preflight/{kind}/{target}":{"get":{"tags":["preflight"],"operationId":"preflightBadge","summary":"Embeddable Preflight badge (SVG)","description":"`preflight: go · 2026-09-12` as an SVG any README can embed, linking to /preflight.\n\nTWO KINDS ONLY — `repo` and `package`. A badge is rendered by whoever opens a README, at a rate nobody controls, so a badge that ran the endpoint or agent lane would turn every README view into a live probe of a third party's server. Those lanes stay on the API, where a caller asks for them on purpose.\n\nThe badge says the verdict word and the date the record carries. Never safe, secure, audited or verified. `unknown` renders neutral grey: we hold no record, which is not a finding about the project.","parameters":[{"name":"kind","in":"path","required":true,"schema":{"type":"string","enum":["repo","package"]}},{"name":"target","in":"path","required":true,"schema":{"type":"string"},"description":"owner/name for a repo (two path segments), or the package name. A scoped npm name keeps its scope."}],"responses":{"200":{"description":"The badge.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Not a repository or package name. A neutral n/a badge is still returned so a README never shows a broken image."},"404":{"description":"A kind other than repo or package. A neutral n/a badge is returned."}}}},"/api/preflight/watch":{"post":{"tags":["preflight"],"operationId":"preflightWatch","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Watch a Preflight target and be told when its verdict changes","description":"Ask to be emailed when the Preflight verdict for a target CHANGES — not on a schedule, not a digest, only on a change.\n\nFREE WHILE IN PREVIEW. A paid tier will be announced; no price is set today, and no payment is accepted here.\n\nWHAT IS CHECKED: the target is put through the same resolver GET /api/preflight uses, and the request is refused if it does not resolve to something we can re-check. A daily job re-runs each watched target and emails only when the verdict differs from the one last recorded.\n\nWHAT IS STORED: the target, an HMAC of your email address (the same construction the request logs use for IPs), the address itself so the notification can be sent, and the last verdict. No IP address, no name, no password. One row, removable.\n\nWHAT A VERDICT IS: a description of what was checked and when. It is never a safety, security or returns judgment, and a change in verdict is a change in what we observed, not a warning.\n\nALWAYS 204 on acceptance — nothing is returned, so this endpoint cannot be used to discover which targets are watched.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"description":"Exactly one target field, plus an address to notify.","properties":{"repo":{"type":"string","description":"GitHub repository URL or bare owner/name."},"package":{"type":"string","description":"npm package name."},"endpoint":{"type":"string","format":"uri","description":"https MCP endpoint."},"agent":{"type":"string","description":"ERC-8004 reference, <chain>:<id>."},"email":{"type":"string","format":"email","description":"Where the change notice goes. Stored so it can be sent, hashed so rows can be counted without reading it."}}}}}},"responses":{"204":{"description":"Accepted. Nothing is returned."},"400":{"description":"No target, more than one target, an unparseable target, or a malformed address."},"429":{"description":"Too many watch requests from one caller."}}}},"/api/route/launch":{"get":{"tags":["route"],"operationId":"routeLaunch","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Choose a launch venue, with the fee disclosed and a deploy config prepared","description":"Where a token launch should go, chosen on each venue's PUBLISHED facts. Venues covered: Clanker v4, Bankr, Virtuals, Zora creator coins.\n\nWHAT THE CHOICE IS: a recommendation. `chosen_by` names each fact the venue was chosen on, and every fact carries the venue page it was read from plus the date it was read (`facts_as_of`) — so the route can be re-derived rather than trusted. A fee schedule says nothing about what a token will do after it launches; nothing here is a security review, an audit or a forecast, and nothing is called best. `null` is unknown and never zero: a venue that publishes no per-party fee split is unread, not generous.\n\nTHE PREPARED CONFIG: for the venue that documents a programmable reward-recipient split (Clanker v4), the response carries a `prepared_deploy.config` in the documented `clanker-sdk` v4 `deploy()` shape. It is JSON to read and sign with your own signer — the SDK is deliberately not a dependency of this service. Other venues are recommend-only and say why no config is emitted. `name`, `symbol` and `deployer` are required for a config; the deployer address signs the deploy and holds the creator reward share, so it is never inferred.\n\nTHE FEE: Sato's slice is one entry in the venue's own reward-recipient list — the deployer takes `10000 - bps`, Sato takes `bps`, and the two always total 10000 bps. It is disclosed in `fee` on EVERY response, including when it is 0, and at 0 no Sato recipient appears in the config at all. It is claimed through the venue's fee locker like any other recipient.\n\nCUSTODY: none. This endpoint never deploys, signs, holds keys or moves funds. A launch that is never signed pays nothing.\n\nPOST takes the same fields as a JSON body, plus a `constraints` object.","parameters":[{"name":"chain","in":"query","required":true,"schema":{"type":"string"},"description":"Chain display name as the directory writes it, e.g. Base or Arbitrum."},{"name":"goal","in":"query","required":true,"schema":{"type":"string","enum":["agent_token","creator_coin","meme","utility"]},"description":"What is being launched, matched against what each venue's documentation covers."},{"name":"name","in":"query","required":false,"schema":{"type":"string","maxLength":64},"description":"Token name. Required for a prepared deploy config."},{"name":"symbol","in":"query","required":false,"schema":{"type":"string","maxLength":16},"description":"Token symbol. Required for a prepared deploy config."},{"name":"deployer","in":"query","required":false,"schema":{"type":"string"},"description":"The 0x address that signs the deploy and holds the creator reward share. Without it no config is prepared."},{"name":"max_pool_fee_pct","in":"query","required":false,"schema":{"type":"number"},"description":"Reject venues whose published pool fee exceeds this percentage. A venue publishing no fee is not excluded — unknown is not disqualifying."},{"name":"require_programmable_fee_split","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Only venues documenting a programmable third-party recipient split."},{"name":"exclude_venues","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated venue slugs to exclude, e.g. clanker,bankr."}],"responses":{"200":{"description":"The chosen venue, the reasons, the disclosed fee and — for the executable lane — a config to sign.","content":{"application/json":{"schema":{"type":"object","required":["route_id","venue","reason","chosen_by","checked_at","fee","facts_as_of","caveat"],"properties":{"route_id":{"type":"string","description":"Identifies THIS launch route, e.g. rt_ab3k9mq2xrtv. Report the deployed token back within 24 hours via POST /api/route/receipt with kind: launch."},"goal":{"type":"string"},"chain":{"type":"string"},"venue":{"type":"object","description":"The chosen venue as the static, cited table records it.","properties":{"slug":{"type":"string"},"name":{"type":"string"},"listing_slug":{"type":"string","nullable":true,"description":"Its Sato Hub listing, when it has one. Null is unknown, not bad."},"lane":{"type":"string","enum":["prepared_config","recommend_only"]},"pool_fee_pct":{"type":"number","nullable":true,"description":"Published pool fee per trade. Null = not published, never 0."},"creator_share_pct":{"type":"number","nullable":true,"description":"The creator's published share of that fee. Null = not published per party."},"protocol_share_pct":{"type":"number","nullable":true},"programmable_fee_split":{"type":"boolean","description":"False means unconfirmed from the venue's own docs on facts_as_of, not impossible."},"facts":{"type":"array","description":"Each fact with the venue page it was read from and the date it was read.","items":{"type":"object","required":["fact","source_url","as_of"],"properties":{"fact":{"type":"string"},"source_url":{"type":"string","format":"uri"},"as_of":{"type":"string","format":"date"}}}},"unconfirmed":{"type":"array","items":{"type":"string"},"description":"What could NOT be confirmed from the venue's documentation. Stated, never smoothed over."},"docs_url":{"type":"string","format":"uri"}}},"reason":{"type":"string","description":"\"<Venue> was chosen by Sato Route on <date>, because …\" — the same framing the swap route uses."},"chosen_by":{"type":"array","items":{"type":"object","required":["signal","value","source_field"],"properties":{"signal":{"type":"string"},"value":{"type":"string"},"source_field":{"type":"string"}}}},"checked_at":{"type":"string","format":"date-time"},"alternatives":{"type":"array","description":"Every other venue that documents this chain, each saying what it is behind on.","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"behind_on":{"type":"string"}}}},"fee":{"type":"object","required":["bps","recipient","basis","disclosed"],"description":"Present on every response, including when bps is 0. A fee disclosed only when it is charged is not a disclosure.","properties":{"bps":{"type":"integer","minimum":0,"maximum":1000,"description":"Sato's share of the creator LP reward split, in basis points. Default 0."},"recipient":{"type":"string","nullable":true,"description":"Null when bps is 0 — and then no Sato recipient is written into the config at all."},"basis":{"type":"string","enum":["creator_lp_share"]},"disclosed":{"type":"boolean","enum":[true]},"note":{"type":"string"}}},"prepared_deploy":{"type":"object","nullable":true,"description":"The config to sign, in the documented clanker-sdk v4 deploy() shape. Null for a recommend-only venue or when name/symbol/deployer were not supplied.","properties":{"sdk":{"type":"string","enum":["clanker-sdk/v4"]},"docs_url":{"type":"string","format":"uri"},"how_to_use":{"type":"string"},"config":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"tokenAdmin":{"type":"string"},"chainId":{"type":"integer"},"context":{"type":"object"},"rewards":{"type":"object","properties":{"recipients":{"type":"array","description":"bps always total exactly 10000. The deployer holds 10000 minus Sato's disclosed share.","items":{"type":"object","properties":{"recipient":{"type":"string"},"admin":{"type":"string"},"bps":{"type":"integer"},"token":{"type":"string","enum":["Clanker","Paired","Both"]}}}}}}}}}},"prepared_deploy_unavailable":{"type":"string","nullable":true,"description":"Why no config was prepared, when none was."},"facts_as_of":{"type":"string","format":"date","description":"When the venue facts were read. They go stale; re-read the source_url before signing."},"caveat":{"type":"string"}}}}}},"400":{"description":"Missing or malformed input. The body names the accepted goals and the chains a venue documents."},"404":{"description":"No venue in the table documents a launch on this chain under these constraints. `{ unavailable, tried: [{ venue, reason, source_url }], supported_chains, supported_goals }` — a reading of the table, not a claim that no venue exists."}}},"post":{"tags":["route"],"operationId":"routeLaunchPost","summary":"Same route, as a JSON body","description":"Identical to GET, with `constraints: { max_pool_fee_pct, require_programmable_fee_split, exclude_venues[] }` as an object. Nothing is deployed, signed or stored beyond the route receipt.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chain","goal"],"properties":{"chain":{"type":"string"},"goal":{"type":"string","enum":["agent_token","creator_coin","meme","utility"]},"name":{"type":"string","maxLength":64},"symbol":{"type":"string","maxLength":16},"deployer":{"type":"string","description":"0x address that signs the deploy."},"constraints":{"type":"object","properties":{"max_pool_fee_pct":{"type":"number"},"require_programmable_fee_split":{"type":"boolean"},"exclude_venues":{"type":"array","items":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"As GET."},"400":{"description":"Missing or malformed input."},"404":{"description":"No venue documents this chain under these constraints."}}}},"/api/satobot/plan":{"get":{"tags":["satobot"],"operationId":"satobotPlan","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"Turn a goal in plain words into a build plan","description":"Describe the onchain agent you want; get a plan you can act on.\n\nWHAT COMES BACK: the goal restated as it was understood (including the parsed intent and which chain was read from your words), a stack of REAL directory listings bucketed into slots — framework, wallet, payments, trading, data, MCP tooling, security — each carrying its Sato Score, liveness, the share of Sato Hub's own daily checks it answered, whether its documented install was reproduced by us, and its `sato_url`; the published deploy spec for every item that has one; a Preflight verdict per item with one evidence line per check; the first action when the goal implies one; the questions you still have to answer; and the next steps.\n\nWHAT IT IS: a recommendation assembled from Sato Hub's own records at `checked_at`. Nothing in a plan is invented — a component that is not a listing cannot appear — and `null` is unknown, never zero. A Sato Score measures how open, active and verifiable a project is; it is not a security review, a quality judgment or a statement about returns. A Preflight `unknown` means Sato Hub holds no record, not that something is wrong.\n\nCUSTODY: none. This endpoint never holds keys, signs, deploys or moves funds. A swap first-action carries a quote taken at a NOMINAL size — your size is yours to choose — and a launch first-action carries a config you read and sign yourself, with the fee disclosed before anything is signed.\n\nPOST takes the same fields as a JSON body, with `constraints` as an array.","parameters":[{"name":"goal","in":"query","required":true,"schema":{"type":"string","maxLength":600},"description":"What you want to build, in plain words, e.g. 'a Base trading agent that swaps USDC to ETH on a signal'."},{"name":"chain","in":"query","required":false,"schema":{"type":"string","maxLength":40},"description":"Chain the agent runs on. When omitted it is read from the goal, and `chain_source` says which."},{"name":"budget_usd","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Rough monthly budget. Restated in the plan; it does not filter the stack."},{"name":"constraints","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated hard constraints to restate back, e.g. self-custody only,no API keys."},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","markdown"]},"description":"'json' (default) or 'markdown' — the short chat render, for a channel adapter."}],"responses":{"200":{"description":"The build plan.","content":{"application/json":{"schema":{"type":"object","required":["goal","restatement","intent","stack","first_action","open_questions","next_steps","caveat","checked_at"],"properties":{"goal":{"type":"string"},"restatement":{"type":"string","description":"The goal as it was understood, so a misread is visible before anything is built on it."},"intent":{"type":"string","enum":["swap","launch","trading","payments","data","identity","general"]},"intent_signals":{"type":"array","items":{"type":"string"},"description":"Which rule matched, and on what words."},"chain":{"type":"string","nullable":true},"chain_source":{"type":"string","enum":["given","parsed","unknown"]},"budget_usd":{"type":"number","nullable":true},"constraints":{"type":"array","items":{"type":"string"}},"matched_use_cases":{"type":"array","items":{"type":"string"}},"stack":{"type":"array","items":{"type":"object","properties":{"slot":{"type":"string"},"slot_label":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string"},"sato_url":{"type":"string","description":"The canonical record. Cite it — it is the live one; this plan is a copy."},"verify_url":{"type":"string","nullable":true},"trust_score":{"type":"integer","nullable":true},"trust_tier":{"type":"string","nullable":true},"liveness_ok":{"type":"boolean","nullable":true},"observed_success_pct":{"type":"number","nullable":true,"description":"Share of Sato Hub's OWN daily checks that succeeded. Never uptime."},"observed_days":{"type":"integer","nullable":true},"install_verified":{"type":"boolean","description":"The documented install was reproduced in an ephemeral container on the spec's date. It says nothing about runtime."},"chains":{"type":"array","items":{"type":"string"}},"why":{"type":"string","description":"The fields this pick was ranked on."},"deploy_spec":{"type":"object","nullable":true,"description":"runtime, install, entry, requires, deploy_status, source_url, as_of — null when the listing publishes none."},"preflight":{"type":"object","nullable":true,"description":"{ verdict: go|caution|no|unknown, rule, checked_at, evidence[], reason }. Null when the item was outside the preflight budget; `unknown` with a reason when the check did not complete."}}}},"gaps":{"type":"array","items":{"type":"string"},"description":"Where the directory has no strong match. Stated, not hidden."},"first_action":{"type":"object","description":"{ kind: none|swap|launch, ready, missing[], note, ... }. A swap carries the route quote; a launch carries the venue route and, when name/symbol/deployer are known, a prepared config."},"open_questions":{"type":"array","items":{"type":"string"}},"next_steps":{"type":"array","items":{"type":"string"}},"citation_ask":{"type":"string"},"caveat":{"type":"string"},"rules":{"type":"array","items":{"type":"string"}},"checked_at":{"type":"string","format":"date-time"},"plan_url":{"type":"string"}}}},"text/markdown":{"schema":{"type":"string","description":"The short chat render, when format=markdown."}}}},"400":{"description":"Missing or malformed goal."}}},"post":{"tags":["satobot"],"operationId":"satobotPlanPost","summary":"Same plan, JSON body","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["goal"],"properties":{"goal":{"type":"string","maxLength":600},"chain":{"type":"string","maxLength":40},"budget_usd":{"type":"number","minimum":0},"constraints":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":10},"format":{"type":"string","enum":["json","markdown"]}}}}}},"responses":{"200":{"description":"As GET."},"400":{"description":"Missing or malformed input."}}}},"/api/satobot/scaffold":{"get":{"tags":["satobot"],"operationId":"satobotScaffold","security":[{},{"bearerAuth":[]},{"satoKey":[]}],"summary":"The build plan, as a starter repo (zip)","description":"Builds the plan for a goal and writes it into a small starter repository, returned as a zip.\n\nWHAT IS IN IT: README.md (the plan rendered, every source URL, and the disclosure that Sato OS is ours) · sato.plan.json (the plan, signed at its own checked_at when a signing key is configured) · .env.example (every environment variable the stack's own deploy specs declare, values BLANK, each under a comment naming which listing wants it) · install.sh (the documented install lines in order, each under its Preflight verdict) · agent.config.json (chain, tokens and the venue Sato Route chose, with chosen_by) · .mcp.json · docker-compose.yml, present only when every stack item documents a docker install.\n\nNOTHING IS GENERATED. Every line is a template constant or a field of the plan, copied verbatim. A value we do not hold is left blank with a comment, never filled with a plausible default.\n\nTHE ONE SAFETY RULE: a documented install that pipes a remote script into a shell is written COMMENTED, with a warning above it — never as an executable line.\n\nDETERMINISTIC: the same plan yields a byte-identical archive; the timestamps come from the plan's checked_at, not the clock. Capped at 2 MB.\n\nNON-CUSTODIAL: the archive holds no keys and no secrets, and nothing here signs, deploys or moves anything.","parameters":[{"name":"goal","in":"query","required":true,"schema":{"type":"string","maxLength":600},"description":"What you want to build, in plain words."},{"name":"chain","in":"query","schema":{"type":"string","maxLength":40},"description":"Chain the agent runs on. Read from the goal when omitted."},{"name":"budget_usd","in":"query","schema":{"type":"number","minimum":0}},{"name":"constraints","in":"query","schema":{"type":"string"},"description":"Comma-separated hard constraints, restated back in the plan."}],"responses":{"200":{"description":"The starter repo. `Content-Disposition` names the file; `Sato-Scaffold-Files` lists its contents.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Missing or malformed goal."},"413":{"description":"The archive exceeded the 2 MB cap."}}},"post":{"tags":["satobot"],"operationId":"satobotScaffoldPost","summary":"Same archive, JSON body — or scaffold a plan you already hold","description":"Identical to GET, plus one thing: post a `plan` you already have (as /api/satobot/plan returned it) and it is written into files without being rebuilt. The archive is byte-identical to the one that plan produces anywhere else.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"goal":{"type":"string","maxLength":600},"chain":{"type":"string","maxLength":40},"budget_usd":{"type":"number","minimum":0},"constraints":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":10},"plan":{"type":"object","description":"A build plan as /api/satobot/plan returns it. When present, goal/chain are ignored and the plan is used verbatim."}}}}}},"responses":{"200":{"description":"As GET."},"400":{"description":"Missing or malformed input."},"413":{"description":"The archive exceeded the 2 MB cap."}}}},"/api/route/receipt":{"post":{"tags":["route"],"operationId":"routeReceipt","summary":"Report back what happened to a route we quoted","description":"Optional. If you signed a route from /api/route/swap, you may tell us what became of it: the `route_id` from that response, the transaction hash, and whether it was submitted, confirmed or failed.\n\nWHAT IS RECORDED: a self-report, accepted as one. We check the shape — that the id is one we minted within the last 24 hours, and that the hash reads as a 0x 32-byte EVM hash or a base58 Solana signature — and update that one row. We do NOT verify here that the transaction exists, landed, or carried the fee; a later on-chain reconciliation reads the chain and marks whether a transfer to the fee address is visible in the transaction. A receipt is an observation. It is never a statement of revenue, profit or performance.\n\nWHAT IS STORED: the venue, chain, token pair and raw amounts already in the quote, the hash you sent, and a salted hash of your IP. Never the IP itself, never the signer address, never a name or an email.\n\nALWAYS 204 — accepted, malformed, unknown id, or store unavailable. Nothing is returned, so this endpoint cannot be used to discover which route ids exist.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["route_id","tx_hash","status"],"properties":{"route_id":{"type":"string","description":"The `route_id` from the /api/route/swap response, e.g. rt_ab3k9mq2xrtv. Valid for 24 hours."},"tx_hash":{"type":"string","description":"0x-prefixed 66-character EVM transaction hash, or a base58 Solana signature."},"status":{"type":"string","enum":["submitted","confirmed","failed"],"description":"What you observed. `quoted` is ours to write, not yours to report."},"amount_out":{"type":"string","nullable":true,"description":"Optional: the output amount actually received, in the output token's base units."},"kind":{"type":"string","enum":["swap","launch"],"description":"Optional. Must match the row we minted — a launch receipt is never reclassified as a swap on request."},"token_address":{"type":"string","nullable":true,"description":"Optional, for a launch: the deployed token contract. Recorded as a self-report of a shape we checked, never as a claim that the token exists or carries the reward split we prepared."}}}}}},"responses":{"204":{"description":"Always. The report was accepted, or it was dropped — the response is the same either way by design."}}}},"/api/badge/{slug}.json":{"get":{"tags":["badges"],"operationId":"getResourceBadgeJson","summary":"Sato Score badge (shields.io endpoint JSON)","description":"The same badge as the SVG, in the shields.io endpoint-badge schema (schemaVersion 1, label, message, color). Render it anywhere shields renders: https://img.shields.io/endpoint?url=https%3A%2F%2Fsatohub.ai%2Fapi%2Fbadge%2F{slug}.json . `message` is the score (with ✓ when an evidence-gated verification state is earned) or `provisional`; `color` is the tier colour. The extra fields `verify_url`, `sato_url` and `caveat` are ignored by shields and kept for machines reading the JSON directly. 404 (with isError) for an unknown, deprecated or non-scoreable listing.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The badge data.","content":{"application/json":{"schema":{"type":"object","required":["schemaVersion","label","message","color"],"properties":{"schemaVersion":{"type":"integer","enum":[1]},"label":{"type":"string"},"message":{"type":"string"},"color":{"type":"string","description":"Hex without #, or a shields colour name."},"labelColor":{"type":"string"},"namedLogo":{"type":"string"},"verify_url":{"type":"string","format":"uri"},"sato_url":{"type":"string","format":"uri"},"caveat":{"type":"string"}}}}}},"404":{"description":"Unknown or non-scoreable slug; body carries isError: true."}}}},"/api/badge/{slug}":{"get":{"tags":["badges"],"operationId":"getResourceBadge","summary":"Sato Score badge (SVG)","description":"An embeddable badge for a listed resource. Hotlink it — no key, no signup. Answers 404 with a placeholder SVG (not JSON) for an unknown, deprecated or non-scoreable listing, so an <img> never renders broken. Non-scoreable means editorial listings — research papers, newsletters, communities — which carry no score by design.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The badge.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"404":{"description":"Placeholder SVG for an unknown or non-scoreable slug.","content":{"image/svg+xml":{"schema":{"type":"string"}}}}}}},"/api/agents/{slug}/badge":{"get":{"tags":["badges"],"operationId":"getAgentBadge","summary":"Registered-on-Sato-Hub badge (SVG)","description":"An embeddable badge for a listed Agent Passport. It says registered, never verified — registration records a claim, it does not endorse the agent.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The badge.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"404":{"description":"Placeholder SVG for an unknown or unlisted agent.","content":{"image/svg+xml":{"schema":{"type":"string"}}}}}}},"/api/agents/challenge":{"get":{"tags":["registry"],"operationId":"getWalletChallenge","summary":"Get a wallet-signature challenge","description":"Step one of self-issuing an Agent Passport. Returns a canonical message binding the agent name, wallet and a timestamp; sign it with the agent's key (EIP-191 personal_sign, EOA only) and register with the signature. Valid for 15 minutes. A verified signature proves control of the KEY — never that the agent is safe or that it does what it claims.","parameters":[{"name":"name","in":"query","required":true,"schema":{"type":"string"},"description":"The agent name you will register."},{"name":"wallet","in":"query","required":true,"schema":{"type":"string"},"description":"The agent's wallet address."}],"responses":{"200":{"description":"The challenge to sign.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"signed_at":{"type":"string","format":"date-time"},"expires_in_seconds":{"type":"integer"},"next":{"type":"string"}}}}}},"400":{"description":"Missing or invalid name/wallet."}}}},"/api/agents/verify":{"post":{"tags":["registry"],"operationId":"verifyAgentOnchain","summary":"Run the on-chain checks for an agent","description":"Agent-callable. Runs each check by its own method and reports them separately: ERC-8004 registration across EVM chains, an MCP handshake, an A2A agent card, x402 presence in the Coinbase Bazaar, and multi-chain wallet activity. Read-only against public sources, and it changes nothing about the agent's trust status — every check reports what is provable, not whether the agent is good.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"wallet":{"type":"string"},"endpoint_url":{"type":"string","format":"uri"},"website_url":{"type":"string","format":"uri"},"payment_endpoint_url":{"type":"string","format":"uri"},"chains":{"type":"array","items":{"type":"string"}},"standards":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The per-check report.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid JSON body."}}}},"/api/claim/start":{"get":{"tags":["claims"],"operationId":"startClaim","summary":"Which domains could prove this listing is yours","description":"Returns the domains derivable from THIS listing plus the token to publish for each. You cannot nominate a domain: candidates come from the listing's own website and docs hosts, which is what stops one project claiming another. Shared platforms (github.com, *.github.io, vercel.app and the like) are excluded, because proving them proves control of the platform rather than the project — such a listing returns an empty `targets` array, which is an answer, not an error.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Claim targets and instructions.","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"already_claimed":{"type":"boolean"},"targets":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string","description":"The target's identity: a hostname, or github.com/<owner>/<repo> for a repo target."},"kind":{"type":"string","enum":["domain","repo"]},"source":{"type":"string"},"token":{"type":"string"},"dnsName":{"type":"string","description":"Domain targets only."},"fileUrl":{"type":"string","format":"uri","description":"Domain targets only."},"repo":{"type":"string","description":"Repo targets only: <owner>/<repo>."},"repoPath":{"type":"string","description":"Repo targets only: the path the file must live at on the default branch."}}}},"instructions":{"type":"object"}}}}}},"401":{"description":"Sign in first."},"404":{"description":"No such listing."}}}},"/api/claim/verify":{"post":{"tags":["claims"],"operationId":"verifyClaim","summary":"Check the published proof","description":"Checks a TXT record at _satohub-challenge.<domain>, or the token served at https://<domain>/.well-known/satohub-claim.txt. A failed check answers 200 with verified:false and a human-readable reason — DNS propagation is a wait, not an error. On success the claim is recorded.\n\nA CLAIM GRANTS NO TRUST. It proves control of a domain. verification_status, the Sato Score and every scored field stay untouched and are not editable by a claimant.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slug","domain","method"],"properties":{"slug":{"type":"string"},"domain":{"type":"string","description":"Must be one of the targets from /api/claim/start."},"method":{"type":"string","enum":["dns","file","repo"]}}}}}},"responses":{"200":{"description":"Result, whether or not the proof was found.","content":{"application/json":{"schema":{"type":"object","properties":{"verified":{"type":"boolean"},"detail":{"type":"string"},"grants":{"type":"string"}}}}}},"400":{"description":"Missing fields, or a domain this listing does not point at."},"401":{"description":"Sign in first."}}}},"/api/claim/edits":{"get":{"tags":["claims"],"operationId":"getClaimEditable","summary":"Current values and what you may change","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Editable fields, current values, and anything already queued.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Sign in first."},"403":{"description":"You have not proven control of this listing."}}},"post":{"tags":["claims"],"operationId":"submitClaimEdits","summary":"Queue corrections to a listing you control","description":"Accepts descriptive fields only: description_short, description_long, website_url, docs_url, github_url, x_url, demo_url, tags, chains_supported. Anything else is rejected by name.\n\nEdits QUEUE rather than apply immediately: they go through the same validation every listing write uses — schema check and banned-phrase scan — and publish on the next daily refresh. An unchanged field queues nothing.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slug","fields"],"properties":{"slug":{"type":"string"},"fields":{"type":"object","description":"field -> new value. Lists accept an array or a comma-separated string."}}}}}},"responses":{"200":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"array","items":{"type":"string"}},"rejected":{"type":"array","items":{"type":"object"}},"note":{"type":"string"}}}}}},"400":{"description":"No editable fields in the submission."},"401":{"description":"Sign in first."},"403":{"description":"You have not proven control of this listing."}}}},"/.well-known/jwks.json":{"get":{"tags":["signing"],"operationId":"getJwks","summary":"Public keys that verify signed responses","description":"An RFC 7517 JWK Set of RFC 8037 OKP keys (kty OKP, crv Ed25519, x the raw 32-byte public key base64url, kid, use sig, alg EdDSA). The active key is first; a retired key stays published for 90 days so a signature stored in a CI log last month still verifies. `{ \"keys\": [] }` means this deployment signs nothing.","parameters":[],"responses":{"200":{"description":"The key set.","content":{"application/jwk-set+json":{"schema":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"type":"object","required":["kty","crv","x","kid","use","alg"],"properties":{"kty":{"type":"string","enum":["OKP"]},"crv":{"type":"string","enum":["Ed25519"]},"x":{"type":"string","description":"Raw 32-byte public key, base64url, unpadded."},"kid":{"type":"string","description":"First 8 hex characters of SHA-256 over x's raw bytes."},"use":{"type":"string","enum":["sig"]},"alg":{"type":"string","enum":["EdDSA"]}}}}}}}}}}}},"/.well-known/sato-score-roots.json":{"get":{"tags":["signing"],"operationId":"getScoreRoots","summary":"The daily Merkle roots over every published Sato Score","description":"One root per UTC day, newest first, for the last 90 days. Each root commits to every Sato Score published that day, is signed with the key at /.well-known/jwks.json, and — where an attestation key is configured — carries the EAS attestation on Base that wrote it to a chain.\n\nThe file also states the leaf and node encoding in full, so an independent implementation can rebuild a root without reading our code. Pair it with /api/verify/{slug}/proof, and fold the proof against THIS file rather than against the proof response: a proof checked only against a root served in the same reply proves nothing.\n\nWHAT A ROOT PROVES: what we published, and when. Not that a score is correct, and not a safety, quality or returns grade. A root with no `onchain` block was not written to a chain and says why in `onchain_note`; `signature: null` means no signing key was configured that day. Absent is published as absent.","parameters":[],"responses":{"200":{"description":"The published roots.","content":{"application/json":{"schema":{"type":"object","required":["version","method","encoding","signature","roots"],"properties":{"version":{"type":"string","enum":["sato-score-roots/v1"]},"issuer":{"type":"string","format":"uri"},"window_days":{"type":"integer"},"method":{"type":"string","description":"The hashing recipe, named on every root."},"encoding":{"type":"object","description":"Leaf preimage, node function, ordering, and which listings have a leaf at all."},"signature":{"type":"object","description":"What the root signature covers, and where the keys are."},"onchain":{"type":"object","description":"The EAS contracts on Base and the schema, with their documentation sources."},"unavailable":{"type":["string","null"],"description":"Non-null when the history could not be read. Unknown, never an empty history."},"count":{"type":"integer"},"roots":{"type":"array","items":{"type":"object","required":["captured_date","root","leaf_count","method"],"properties":{"captured_date":{"type":"string","format":"date"},"root":{"type":"string","description":"Lowercase hex, no 0x."},"leaf_count":{"type":"integer"},"method":{"type":"string"},"signature":{"type":["object","null"]},"onchain":{"type":"object","description":"Present only when the root was written to a chain."},"onchain_note":{"type":"string","description":"Why it was not."}}}}}}}}}}}},"/api/verify/{slug}/proof":{"get":{"tags":["signing"],"operationId":"getScoreProof","summary":"A Merkle proof that one listing's Sato Score was in that day's published root","description":"Returns the listing's record as committed, its leaf digest and preimage, the sibling digests that fold it to the root, and the root itself with its signature and any on-chain attestation.\n\nWith no `date`, answers for the newest day that has a published root, looking back at most seven days. The response is itself signed (Sato-Signature), which is not the interesting part: verify the proof against an archived copy of /.well-known/sato-score-roots.json instead.\n\n`proof: []` is legitimate on a single-listing day, where the leaf is the root. A listing with no leaf answers 404 with the reason — editorial listings (research papers, newsletters, communities) are never scored and therefore never have one.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"date","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"UTC day, YYYY-MM-DD. Defaults to the newest published root."}],"responses":{"200":{"description":"The proof.","content":{"application/json":{"schema":{"type":"object","required":["slug","captured_date","record","leaf","proof","root"],"properties":{"slug":{"type":"string"},"captured_date":{"type":"string","format":"date"},"record":{"type":"object","properties":{"slug":{"type":"string"},"trust_score":{"type":"integer"},"trust_tier":{"type":"string"},"captured_date":{"type":"string"}}},"leaf":{"type":"string"},"leaf_preimage":{"type":"string","description":"The exact bytes the leaf digest was taken over."},"proof":{"type":"array","items":{"type":"string"},"description":"Sibling digests, bottom-up. Fold with node(a,b) = SHA256(domain || min || max)."},"root":{"type":"string"},"leaf_count":{"type":"integer"},"method":{"type":"string"},"rebuilt_root":{"type":"string","description":"The root recomputed from the snapshot at request time."},"matches_published_root":{"type":"boolean","description":"False means the committed root and a rebuild disagree — an integrity problem, surfaced rather than hidden."},"root_signature":{"type":["object","null"]},"onchain":{"type":["object","null"]},"onchain_note":{"type":["string","null"]}}}}}},"404":{"description":"No root for that day, or no leaf for that listing. The body says which."},"503":{"description":"This deployment holds no score history."}}}},"/.well-known/sato-signing.json":{"get":{"tags":["signing"],"operationId":"getSigningPolicy","summary":"What is signed, how, and for how long the keys stay checkable","description":"The JWKS says which key. This says what it covers: the header names, the two signed messages (the exact bytes, and the canonical body with meta.signature removed), the canonicalisation rule, which surfaces are signed, the rotation policy, and what a valid signature does NOT mean.","parameters":[],"responses":{"200":{"description":"The signing policy.","content":{"application/json":{"schema":{"type":"object","required":["version","algorithm","keys","headers","covered","rotation","surfaces"],"properties":{"version":{"type":"string","enum":["sato-signing/v1"]},"issuer":{"type":"string","format":"uri"},"algorithm":{"type":"object"},"keys":{"type":"object","description":"jwks_url, active_kid (null when unsigned), and how the kid is derived."},"headers":{"type":"object"},"covered":{"type":"object","description":"The exact message constructions and the canonical-JSON rule."},"unsigned":{"type":"object"},"rotation":{"type":"object"},"surfaces":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"what":{"type":"string"}}}},"limits":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/projects":{"post":{"tags":["projects"],"operationId":"createProject","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Create a private project","description":"The owner is resolved from the Privy bearer. A body that names `owner_account_id`, `id`, `visibility`, `archived_at` or a timestamp is REFUSED with policy_denied rather than ignored — silently dropping a field somebody deliberately sent teaches them it was accepted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","description":"A private project. `owner_account_id` is deliberately absent: it is an internal key, and returning it invites a client to send it back.","required":["id","name","visibility","created_at","updated_at","row_version"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"visibility":{"type":"string","enum":["private"],"description":"There is no other value in A1."},"archived_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"Optimistic-concurrency token. A PATCH must echo the value it read, or it is refused with 409."},"session_ids":{"type":"array","items":{"type":"string"}},"version_count":{"type":"integer"},"latest_version_id":{"type":["string","null"],"format":"uuid"}}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — the body tried to set something the server derives (an owner, an evidence source or label).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"503":{"description":"503 — a required piece of infrastructure (the database, the artifact store) is not configured here.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}},"get":{"tags":["projects"],"operationId":"listProjects","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"List your projects","description":"Only the caller's own projects. Archived ones are excluded unless `archived=1`.","parameters":[{"name":"archived","in":"query","required":false,"schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Your projects, most recently updated first.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","description":"A private project. `owner_account_id` is deliberately absent: it is an internal key, and returning it invites a client to send it back.","required":["id","name","visibility","created_at","updated_at","row_version"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"visibility":{"type":"string","enum":["private"],"description":"There is no other value in A1."},"archived_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"Optimistic-concurrency token. A PATCH must echo the value it read, or it is refused with 409."},"session_ids":{"type":"array","items":{"type":"string"}},"version_count":{"type":"integer"},"latest_version_id":{"type":["string","null"],"format":"uuid"}}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/projects/{id}":{"get":{"tags":["projects"],"operationId":"getProject","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Read one project","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The project.","content":{"application/json":{"schema":{"type":"object","description":"A private project. `owner_account_id` is deliberately absent: it is an internal key, and returning it invites a client to send it back.","required":["id","name","visibility","created_at","updated_at","row_version"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"visibility":{"type":"string","enum":["private"],"description":"There is no other value in A1."},"archived_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"Optimistic-concurrency token. A PATCH must echo the value it read, or it is refused with 409."},"session_ids":{"type":"array","items":{"type":"string"}},"version_count":{"type":"integer"},"latest_version_id":{"type":["string","null"],"format":"uuid"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}},"patch":{"tags":["projects"],"operationId":"patchProject","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Update the draft","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists. `row_version` is REQUIRED — a PATCH without one is a lost update waiting to happen and is refused with 409. `session_id` links a builder chat session, which must belong to the same account; a cross-account link is policy_denied and nothing is written.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["row_version"],"properties":{"row_version":{"type":"integer"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"session_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","description":"A private project. `owner_account_id` is deliberately absent: it is an internal key, and returning it invites a client to send it back.","required":["id","name","visibility","created_at","updated_at","row_version"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"visibility":{"type":"string","enum":["private"],"description":"There is no other value in A1."},"archived_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"Optimistic-concurrency token. A PATCH must echo the value it read, or it is refused with 409."},"session_ids":{"type":"array","items":{"type":"string"}},"version_count":{"type":"integer"},"latest_version_id":{"type":["string","null"],"format":"uuid"}}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — the body tried to set something the server derives (an owner, an evidence source or label).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the object changed since you read it (row_version), a parent version is not in this project, or these files are already sealed with different sealed metadata (the detail names the fields).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/projects/{id}/archive":{"post":{"tags":["projects"],"operationId":"archiveProject","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Archive a project","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists. Idempotent: archiving an archived project returns it unchanged. Archiving is a shelf, not a delete — versions, evidence and artifacts all stay, and the owner can still download them.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Archived.","content":{"application/json":{"schema":{"type":"object","description":"A private project. `owner_account_id` is deliberately absent: it is an internal key, and returning it invites a client to send it back.","required":["id","name","visibility","created_at","updated_at","row_version"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"visibility":{"type":"string","enum":["private"],"description":"There is no other value in A1."},"archived_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"Optimistic-concurrency token. A PATCH must echo the value it read, or it is refused with 409."},"session_ids":{"type":"array","items":{"type":"string"}},"version_count":{"type":"integer"},"latest_version_id":{"type":["string","null"],"format":"uuid"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/projects/adopt":{"post":{"tags":["projects"],"operationId":"adoptDraft","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Adopt a browser-held anonymous draft","description":"Creates a project for the CALLER from a draft the browser held while signed out. The draft is untrusted input: any account id, project id or ownership claim inside it is discarded. A `session_id` is linked only if that session already belongs to the caller; an unrecognised one is ignored rather than failing the request, so a stale value in localStorage cannot strand the user's work.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"session_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Adopted.","content":{"application/json":{"schema":{"type":"object","description":"A private project. `owner_account_id` is deliberately absent: it is an internal key, and returning it invites a client to send it back.","required":["id","name","visibility","created_at","updated_at","row_version"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"task_spec":{"type":"object","additionalProperties":true},"visibility":{"type":"string","enum":["private"],"description":"There is no other value in A1."},"archived_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"Optimistic-concurrency token. A PATCH must echo the value it read, or it is refused with 409."},"session_ids":{"type":"array","items":{"type":"string"}},"version_count":{"type":"integer"},"latest_version_id":{"type":["string","null"],"format":"uuid"}}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/projects/{id}/versions":{"post":{"tags":["projects"],"operationId":"sealVersion","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Seal an immutable version","description":"Hashes the files, runs the export guards, writes the archive to private storage, verifies its CONTENT landed, and records the version. IDENTICAL INPUTS RETURN THE EXISTING VERSION WITH 200, not 201 — the digest is the identity, so re-sealing is a no-op rather than a duplicate. THE SAME FILES WITH DIFFERENT SEALED METADATA ARE A 409, naming the fields that differ: because the digest is the identity, the same files cannot be sealed twice with different claims, and answering 200 with the first version's metadata would discard the second caller's without it noticing. A `parent_version_id` outside this project is 409. Sealing is a deterministic transformation of data you already produced and is not charged; nothing here executes anything. Send `recipe_id` instead of `files` to seal one of this build's executable recipes: the package is assembled on the server from the same file list the CI harness tests, so the sealed digest is the tested digest. An unknown recipe is 422 with the known list. Send `use_task_spec: true` (with neither `files` nor `recipe_id`) to seal the recipe named in the project's own `task_spec.recipe_id` — the one the builder's Save project stored; a project whose task spec names no recipe is 422. The sealed provenance carries the build's git revision when one is known.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recipe_id":{"type":"string"},"use_task_spec":{"type":"boolean","description":"true = seal the recipe in this project's task_spec.recipe_id. Ignored when files or recipe_id is sent."},"files":{"type":"array","minItems":1,"items":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string"},"content":{"type":"string"}}}},"manifest":{"type":"object","additionalProperties":true},"io_schemas":{"type":"object","additionalProperties":true},"runtime":{"type":"object","additionalProperties":true},"model_policy":{"type":"object","additionalProperties":true},"dependency_lock":{"type":"object","additionalProperties":true},"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}},"declared_secret_names":{"type":"array","items":{"type":"string"},"description":"NAMES ONLY. A secret value must never be sent here or anywhere else."},"licenses":{"type":"array","items":{}},"provenance":{"type":"object","additionalProperties":true},"parent_version_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"These inputs were already sealed; this is the existing version.","content":{"application/json":{"schema":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/version/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/version/v1.json"}}}}},"201":{"description":"Sealed.","content":{"application/json":{"schema":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/version/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/version/v1.json"}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"The files failed an export guard — path traversal, a symlink, a size cap or apparent secret material.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the object changed since you read it (row_version), a parent version is not in this project, or these files are already sealed with different sealed metadata (the detail names the fields).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"503":{"description":"503 — a required piece of infrastructure (the database, the artifact store) is not configured here.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}},"get":{"tags":["projects"],"operationId":"listVersions","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"List a project's versions","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Versions, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/version/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/version/v1.json"}}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/versions/{id}":{"get":{"tags":["projects"],"operationId":"getVersion","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Read a sealed version","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists. Includes the version's evidence and its mutable ops metadata. `evidence_for_digest` adds platform results recorded against any version with the same artifact digest (source = platform only; no version or account identifiers). `artifact_ref` is never returned: it is a storage key.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The version.","content":{"application/json":{"schema":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/version/v1.json","externalDocs":{"url":"https://satohub.ai/schemas/version/v1.json"}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/versions/{id}/artifact":{"get":{"tags":["projects"],"operationId":"downloadArtifact","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Download the sealed archive","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists. Answers 302 to a signed URL that expires in 600 seconds, with `Cache-Control: no-store`. Recompute the artifact digest from what you download — that is the check this whole record exists to make possible. When no artifact store is configured the answer is 503 missing_requirement; there is never a public fallback location.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"302":{"description":"Redirect to a short-lived signed URL.","headers":{"Location":{"schema":{"type":"string","format":"uri"}},"Sato-Artifact-Digest":{"schema":{"type":"string"},"description":"The digest this archive must hash to."},"Sato-Artifact-Expires-In":{"schema":{"type":"integer"},"description":"Seconds the URL remains valid."}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the stored artifact no longer matches the sealed record, or a runner attested a different digest.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"503":{"description":"503 — a required piece of infrastructure (the database, the artifact store) is not configured here.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/versions/{id}/manifest":{"get":{"tags":["projects"],"operationId":"downloadManifest","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Download the signed manifest","description":"Owner only. A caller who does not own this object receives 404, never 403 — a 403 would confirm the object exists. Answers 200 with the version's manifest exactly as it was sealed, as an attachment named `sato-manifest-<first 12 hex of the digest>.json`, with `Cache-Control: no-store`. Keep it beside the archive from /artifact: `node verify-release.mjs <package.zip> <manifest.json> [--jwks <file|url>]` (served at https://satohub.ai/verify-release.mjs ) recomputes the files digest, checks the archive size, and verifies `meta.signature` against https://satohub.ai/.well-known/jwks.json — or against a JWKS file you saved, fully offline. A version sealed with no signing key configured has `meta.signature: null` and verifies as unsigned, never as valid. A passing check proves origin and integrity of these files and claims; it is not a review of what the package does.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The stored signed manifest.","headers":{"Content-Disposition":{"schema":{"type":"string"},"description":"attachment; filename=\"sato-manifest-<12 hex>.json\""},"Sato-Artifact-Digest":{"schema":{"type":"string"},"description":"The digest the manifest was sealed over."}},"content":{"application/json":{"schema":{"type":"object","description":"JSON Schema: https://satohub.ai/schemas/version/v1.json#/properties/manifest","externalDocs":{"url":"https://satohub.ai/schemas/version/v1.json#/properties/manifest"}}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the sealed record carries no manifest.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/versions/{id}/evidence":{"post":{"tags":["projects"],"operationId":"attachEvidence","x-sato-contract":"projects/v1","security":[{"bearerAuth":[]}],"summary":"Attach an observation to a sealed version","description":"TWO CREDENTIALS, never merged. A Privy bearer records CREATOR evidence: `source='creator'`, `label='user-reported'`, and the caller must own the version. A signed platform runner (`x-sato-runner-id`, `x-sato-runner-ts`, `x-sato-runner-nonce`, `x-sato-runner-sig`) records PLATFORM evidence and may choose a label only from that runner's configured set.\n\n`source` and `label` are derived from the credential. A creator body carrying either is refused with policy_denied, not ignored — a creator can never obtain a platform label. A runner must NAME the `version_id` and `artifact_digest` it attests, and a mismatch is stale_evidence; the digest actually stored is always read from the version row.\n\n`result: 'unknown'` with `reason_code: 'isolation_unavailable'` means NOTHING RAN. It is not a failure and must not be displayed as one.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["test_id","test_version","result"],"properties":{"test_id":{"type":"string"},"test_version":{"type":"string"},"result":{"type":"string","enum":["pass","fail","unknown"]},"reason_code":{"type":"string"},"details":{"type":"object","additionalProperties":true},"expires_at":{"type":"string","format":"date-time"},"label":{"type":"string","enum":["install-tested","task-tested","integration-tested","simulation","observed-execution"],"description":"PLATFORM CALLERS ONLY, and only a label that runner is configured to assert. A creator sending this is refused."},"version_id":{"type":"string","format":"uuid","description":"Platform callers: required, and must match the path."},"artifact_digest":{"type":"string","description":"Platform callers: required, and must match the version's digest."},"environment_digest":{"type":"string","description":"Platform callers only."},"trace_ref":{"type":"string","description":"Platform callers only."}}}}}},"responses":{"201":{"description":"Recorded. Evidence is append-only; there is no edit and no delete.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"401":{"description":"Not signed in, or the runner credential was rejected (unknown runner, bad signature, stale timestamp, replayed nonce).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — the body tried to set something the server derives (an owner, an evidence source or label).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such object under this account. The same answer covers 'does not exist', 'not yours' and 'feature disabled'.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the stored artifact no longer matches the sealed record, or a runner attested a different digest.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"503":{"description":"No platform runner is configured in this environment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings":{"get":{"tags":["listings"],"operationId":"searchListings","x-sato-contract":"listings/v1","summary":"Search published agent packages","description":"Public, no auth, rate-limited. Only `published` listings. Cursor pagination: pass `next_cursor` back as `cursor`.","parameters":[{"name":"q","in":"query","schema":{"type":"string","maxLength":100}},{"name":"recipe","in":"query","schema":{"type":"string"}},{"name":"license","in":"query","schema":{"type":"string"}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"A page of listings.","content":{"application/json":{"schema":{"type":"object","required":["listings","next_cursor"],"properties":{"listings":{"type":"array","items":{"type":"object","description":"The public view of a published listing. Never carries an account id, project id, version id, session id, customer parameters, a storage key or a signed URL.","required":["slug","state","title","task","description","creator","version","recipe","inputs","outputs","runtime","required_services","required_env_names","model_support","operating_cost_basis","license","license_source","fork_permitted","support","attribution","screenshots","evidence_freshness","integrity","acquisition_modes","published_at","sato_url","manifest_url"],"properties":{"slug":{"type":"string"},"state":{"type":"string","enum":["published"]},"title":{"type":"string"},"task":{"type":"string"},"description":{"type":"string"},"creator":{"type":"object","properties":{"display_name":{"type":["string","null"]}}},"version":{"type":"object","properties":{"digest":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"sealed_at":{"type":"string","format":"date-time"},"schema_version":{"type":"integer"},"signing":{"type":"string","enum":["signed","unavailable"]},"artifact_bytes":{"type":"integer"}}},"recipe":{"type":"object","properties":{"id":{"type":["string","null"]},"version":{"type":["string","null"]},"revision":{"type":["string","null"]},"title":{"type":["string","null"]},"summary":{"type":["string","null"]}}},"inputs":{"description":"The version's input JSON schema."},"outputs":{"description":"The version's output JSON schema."},"runtime":{"type":"object","additionalProperties":true},"required_services":{"type":"object","properties":{"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}}}},"required_env_names":{"type":["array","null"],"items":{"type":"string"},"description":"Names only, and only when the owner opted in."},"model_support":{"type":"object","properties":{"policy":{"type":["object","null"],"additionalProperties":true},"label":{"type":"string","enum":["configured, not evaluated"]}}},"operating_cost_basis":{"type":["string","null"]},"license":{"type":"string","description":"SPDX id chosen at publish."},"license_source":{"type":"string","enum":["declared","owner_choice"]},"fork_permitted":{"type":"boolean"},"support":{"type":["string","null"]},"attribution":{"type":["string","null"]},"screenshots":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string"}}}},"evidence_freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"integrity":{"type":"string","enum":["ok","stale_evidence","unchecked"]},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"published_at":{"type":["string","null"],"format":"date-time"},"sato_url":{"type":"string","format":"uri"},"manifest_url":{"type":"string","format":"uri","description":"The public signed release manifest (GET /api/listings/{slug}/manifest) for offline verification."}}}},"next_cursor":{"type":["string","null"]}}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{slug}":{"get":{"tags":["listings"],"operationId":"inspectListing","x-sato-contract":"listings/v1","summary":"Inspect one published listing","description":"Public fields only. 404 on the very next request after an unpublish or suspension.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The listing.","content":{"application/json":{"schema":{"type":"object","description":"The public view of a published listing. Never carries an account id, project id, version id, session id, customer parameters, a storage key or a signed URL.","required":["slug","state","title","task","description","creator","version","recipe","inputs","outputs","runtime","required_services","required_env_names","model_support","operating_cost_basis","license","license_source","fork_permitted","support","attribution","screenshots","evidence_freshness","integrity","acquisition_modes","published_at","sato_url","manifest_url"],"properties":{"slug":{"type":"string"},"state":{"type":"string","enum":["published"]},"title":{"type":"string"},"task":{"type":"string"},"description":{"type":"string"},"creator":{"type":"object","properties":{"display_name":{"type":["string","null"]}}},"version":{"type":"object","properties":{"digest":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"sealed_at":{"type":"string","format":"date-time"},"schema_version":{"type":"integer"},"signing":{"type":"string","enum":["signed","unavailable"]},"artifact_bytes":{"type":"integer"}}},"recipe":{"type":"object","properties":{"id":{"type":["string","null"]},"version":{"type":["string","null"]},"revision":{"type":["string","null"]},"title":{"type":["string","null"]},"summary":{"type":["string","null"]}}},"inputs":{"description":"The version's input JSON schema."},"outputs":{"description":"The version's output JSON schema."},"runtime":{"type":"object","additionalProperties":true},"required_services":{"type":"object","properties":{"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}}}},"required_env_names":{"type":["array","null"],"items":{"type":"string"},"description":"Names only, and only when the owner opted in."},"model_support":{"type":"object","properties":{"policy":{"type":["object","null"],"additionalProperties":true},"label":{"type":"string","enum":["configured, not evaluated"]}}},"operating_cost_basis":{"type":["string","null"]},"license":{"type":"string","description":"SPDX id chosen at publish."},"license_source":{"type":"string","enum":["declared","owner_choice"]},"fork_permitted":{"type":"boolean"},"support":{"type":["string","null"]},"attribution":{"type":["string","null"]},"screenshots":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string"}}}},"evidence_freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"integrity":{"type":"string","enum":["ok","stale_evidence","unchecked"]},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"published_at":{"type":["string","null"],"format":"date-time"},"sato_url":{"type":"string","format":"uri"},"manifest_url":{"type":"string","format":"uri","description":"The public signed release manifest (GET /api/listings/{slug}/manifest) for offline verification."}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{slug}/evidence":{"get":{"tags":["listings"],"operationId":"listingEvidence","x-sato-contract":"listings/v1","summary":"Evidence for the listing's exact digest","description":"Source label, test id, result and freshness for rows whose digest equals the listing's. Never raw traces, runner ids or details. Creator-source rows appear only if the owner opted in, with the label kept. No rows → freshness `not_checked`.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Evidence.","content":{"application/json":{"schema":{"type":"object","required":["slug","version_digest","freshness","evidence","method","sato_url"],"properties":{"slug":{"type":"string"},"version_digest":{"type":"string"},"freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"evidence":{"type":"array","items":{"type":"object","properties":{"test_id":{"type":"string"},"test_version":{"type":"string"},"result":{"type":"string","enum":["pass","fail","unknown"]},"reason_code":{"type":["string","null"]},"label":{"type":"string"},"source":{"type":"string","enum":["platform","creator"]},"observed_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"]},"fresh":{"type":"boolean"}}}},"method":{"type":"string"},"sato_url":{"type":"string","format":"uri"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{slug}/manifest":{"get":{"tags":["listings"],"operationId":"listingReleaseManifest","x-sato-contract":"listings/v1","summary":"The public signed release manifest","description":"A `sato.release/v1` document: artifact_digest, artifact_bytes, recipe, license, acquisition modes and evidence freshness, with an Ed25519 `meta.signature` over the canonical body (keys at https://satohub.ai/.well-known/jwks.json). Check a downloaded zip offline with `node verify-release.mjs <package.zip> manifest.json`; the script is served at https://satohub.ai/verify-release.mjs . A pass means the bytes are what Sato Hub sealed and signed; it is not a review of the code and says nothing about safety or results. No project, version, session or account id. Without a signing key: `signing: \"unavailable\"`, `meta.signature: null`. `Cache-Control: no-store`, `X-Robots-Tag: noindex`, `Link: <https://satohub.ai/schemas/release/v1.json>; rel=\"describedby\"`. 404 unless published.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The manifest.","content":{"application/json":{"schema":{"type":"object","description":"sato.release/v1 — full JSON Schema at https://satohub.ai/schemas/release/v1.json.","required":["schema","slug","artifact_digest","artifact_bytes","sealed_at","license","signing","meta"],"additionalProperties":true}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{slug}/download":{"get":{"tags":["listings"],"operationId":"downloadListing","x-sato-contract":"listings/v1","summary":"Download the sealed package","description":"302 to a 600-second signed URL, `Cache-Control: no-store`, `X-Robots-Tag: noindex`. Only when the listing is published AND `download` is in its modes; otherwise 404. Every download is recorded as a listing event. A stored artifact that no longer matches the seal is 409 stale_evidence.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to a short-lived signed URL."},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the stored artifact no longer matches the sealed record. Download is withheld until an operator looks.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"503":{"description":"503 — the database, the artifact store or the hashing secret is not configured here.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{slug}/fork":{"post":{"tags":["listings"],"operationId":"forkListing","x-sato-contract":"listings/v1","security":[{"bearerAuth":[]}],"summary":"Fork into a new private project","description":"Creates a new private project for the CALLER with task_spec {recipe_id, recipe_version, forked_from}. Copies no parameters, evidence, history or credentials, and seals nothing. Refused 403 policy_denied, naming the license, when the listing's license does not permit it (an unknown SPDX id is denied).","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Forked.","content":{"application/json":{"schema":{"type":"object","required":["project","forked_from"],"properties":{"project":{"type":"object","additionalProperties":true},"forked_from":{"type":"object","properties":{"slug":{"type":"string"},"version_digest":{"type":"string"},"license":{"type":"string"}}}}}}}},"401":{"description":"401 policy_denied — not signed in (owner routes take only `Authorization: Bearer <Privy token>`; cookies never work).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — refused by policy: a body field the server derives, a secret-scan hit in an owner-entered field, a license that does not permit fork, or a state transition that is not allowed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{slug}/report":{"post":{"tags":["listings"],"operationId":"reportListing","x-sato-contract":"listings/v1","summary":"Report a published listing","description":"No auth. The reporter is stored only as an HMAC hash (never an IP or account). `note` ≤ 500 characters.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","enum":["secret_exposed","malicious","license","misleading","broken","other"]},"note":{"type":"string","maxLength":500}}}}}},"responses":{"202":{"description":"Recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"recorded":{"type":"boolean"}}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"429":{"description":"429 — too many requests from this caller; retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"503":{"description":"503 — the database, the artifact store or the hashing secret is not configured here.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/projects/{id}/listings":{"post":{"tags":["listings"],"operationId":"createListingDraft","x-sato-contract":"listings/v1","security":[{"bearerAuth":[]}],"summary":"Create a listing draft from a sealed version","description":"Owner only. Copies the allowlisted version fields and stores the owner's form fields after a secret scan (a hit is a refusal, never an override). The version must belong to this project. Returns the owner view with `preview` — exactly what the public page would show.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["version_id","slug","title","task","description","license","acquisition_modes"],"properties":{"version_id":{"type":"string","format":"uuid"},"slug":{"type":"string","pattern":"^[a-z0-9](?:[a-z0-9-]{1,62}[a-z0-9])$"},"title":{"type":"string","minLength":1,"maxLength":120},"task":{"type":"string","minLength":1,"maxLength":280},"description":{"type":"string","minLength":1,"maxLength":4000},"license":{"type":"string","description":"SPDX id. One of the version's declared licenses, or an explicit owner choice (recorded as license_source=owner_choice)."},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]},"minItems":1},"operating_cost_basis":{"type":"string","maxLength":1000},"support":{"type":"string","maxLength":1000},"attribution":{"type":"string","maxLength":1000},"screenshots":{"type":"array","maxItems":4,"items":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string","maxLength":200}}}},"show_env_names":{"type":"boolean"},"include_creator_evidence":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Draft created.","content":{"application/json":{"schema":{"type":"object","description":"The owner's view of their own listing. Owner routes only.","required":["id","slug","state","project_id","version_id","license","acquisition_modes","row_version","preview"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"state":{"type":"string","enum":["draft","submitted","published","rejected","suspended","unpublished"]},"project_id":{"type":"string","format":"uuid"},"version_id":{"type":"string","format":"uuid"},"license":{"type":"string"},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"public_fields":{"type":"object","additionalProperties":true},"owner_fields":{"type":"object","additionalProperties":true},"review_reason":{"type":["string","null"]},"suspend_reason":{"type":["string","null"]},"submitted_at":{"type":["string","null"]},"published_at":{"type":["string","null"]},"unpublished_at":{"type":["string","null"]},"suspended_at":{"type":["string","null"]},"superseded_by":{"type":["string","null"]},"row_version":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"preview":{"type":"object","description":"The public view of a published listing. Never carries an account id, project id, version id, session id, customer parameters, a storage key or a signed URL.","required":["slug","state","title","task","description","creator","version","recipe","inputs","outputs","runtime","required_services","required_env_names","model_support","operating_cost_basis","license","license_source","fork_permitted","support","attribution","screenshots","evidence_freshness","integrity","acquisition_modes","published_at","sato_url","manifest_url"],"properties":{"slug":{"type":"string"},"state":{"type":"string","enum":["published"]},"title":{"type":"string"},"task":{"type":"string"},"description":{"type":"string"},"creator":{"type":"object","properties":{"display_name":{"type":["string","null"]}}},"version":{"type":"object","properties":{"digest":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"sealed_at":{"type":"string","format":"date-time"},"schema_version":{"type":"integer"},"signing":{"type":"string","enum":["signed","unavailable"]},"artifact_bytes":{"type":"integer"}}},"recipe":{"type":"object","properties":{"id":{"type":["string","null"]},"version":{"type":["string","null"]},"revision":{"type":["string","null"]},"title":{"type":["string","null"]},"summary":{"type":["string","null"]}}},"inputs":{"description":"The version's input JSON schema."},"outputs":{"description":"The version's output JSON schema."},"runtime":{"type":"object","additionalProperties":true},"required_services":{"type":"object","properties":{"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}}}},"required_env_names":{"type":["array","null"],"items":{"type":"string"},"description":"Names only, and only when the owner opted in."},"model_support":{"type":"object","properties":{"policy":{"type":["object","null"],"additionalProperties":true},"label":{"type":"string","enum":["configured, not evaluated"]}}},"operating_cost_basis":{"type":["string","null"]},"license":{"type":"string","description":"SPDX id chosen at publish."},"license_source":{"type":"string","enum":["declared","owner_choice"]},"fork_permitted":{"type":"boolean"},"support":{"type":["string","null"]},"attribution":{"type":["string","null"]},"screenshots":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string"}}}},"evidence_freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"integrity":{"type":"string","enum":["ok","stale_evidence","unchecked"]},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"published_at":{"type":["string","null"],"format":"date-time"},"sato_url":{"type":"string","format":"uri"},"manifest_url":{"type":"string","format":"uri","description":"The public signed release manifest (GET /api/listings/{slug}/manifest) for offline verification."}}}}}}}},"400":{"description":"400 — the request is malformed or names an unsupported value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"401":{"description":"401 policy_denied — not signed in (owner routes take only `Authorization: Bearer <Privy token>`; cookies never work).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — refused by policy: a body field the server derives, a secret-scan hit in an owner-entered field, a license that does not permit fork, or a state transition that is not allowed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the slug belongs to another account or is already live, or the listing changed state since you read it.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}},"get":{"tags":["listings"],"operationId":"listProjectListings","x-sato-contract":"listings/v1","security":[{"bearerAuth":[]}],"summary":"The owner's listings for one project","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Listings, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","description":"The owner's view of their own listing. Owner routes only.","required":["id","slug","state","project_id","version_id","license","acquisition_modes","row_version","preview"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"state":{"type":"string","enum":["draft","submitted","published","rejected","suspended","unpublished"]},"project_id":{"type":"string","format":"uuid"},"version_id":{"type":"string","format":"uuid"},"license":{"type":"string"},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"public_fields":{"type":"object","additionalProperties":true},"owner_fields":{"type":"object","additionalProperties":true},"review_reason":{"type":["string","null"]},"suspend_reason":{"type":["string","null"]},"submitted_at":{"type":["string","null"]},"published_at":{"type":["string","null"]},"unpublished_at":{"type":["string","null"]},"suspended_at":{"type":["string","null"]},"superseded_by":{"type":["string","null"]},"row_version":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"preview":{"type":"object","description":"The public view of a published listing. Never carries an account id, project id, version id, session id, customer parameters, a storage key or a signed URL.","required":["slug","state","title","task","description","creator","version","recipe","inputs","outputs","runtime","required_services","required_env_names","model_support","operating_cost_basis","license","license_source","fork_permitted","support","attribution","screenshots","evidence_freshness","integrity","acquisition_modes","published_at","sato_url","manifest_url"],"properties":{"slug":{"type":"string"},"state":{"type":"string","enum":["published"]},"title":{"type":"string"},"task":{"type":"string"},"description":{"type":"string"},"creator":{"type":"object","properties":{"display_name":{"type":["string","null"]}}},"version":{"type":"object","properties":{"digest":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"sealed_at":{"type":"string","format":"date-time"},"schema_version":{"type":"integer"},"signing":{"type":"string","enum":["signed","unavailable"]},"artifact_bytes":{"type":"integer"}}},"recipe":{"type":"object","properties":{"id":{"type":["string","null"]},"version":{"type":["string","null"]},"revision":{"type":["string","null"]},"title":{"type":["string","null"]},"summary":{"type":["string","null"]}}},"inputs":{"description":"The version's input JSON schema."},"outputs":{"description":"The version's output JSON schema."},"runtime":{"type":"object","additionalProperties":true},"required_services":{"type":"object","properties":{"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}}}},"required_env_names":{"type":["array","null"],"items":{"type":"string"},"description":"Names only, and only when the owner opted in."},"model_support":{"type":"object","properties":{"policy":{"type":["object","null"],"additionalProperties":true},"label":{"type":"string","enum":["configured, not evaluated"]}}},"operating_cost_basis":{"type":["string","null"]},"license":{"type":"string","description":"SPDX id chosen at publish."},"license_source":{"type":"string","enum":["declared","owner_choice"]},"fork_permitted":{"type":"boolean"},"support":{"type":["string","null"]},"attribution":{"type":["string","null"]},"screenshots":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string"}}}},"evidence_freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"integrity":{"type":"string","enum":["ok","stale_evidence","unchecked"]},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"published_at":{"type":["string","null"],"format":"date-time"},"sato_url":{"type":"string","format":"uri"},"manifest_url":{"type":"string","format":"uri","description":"The public signed release manifest (GET /api/listings/{slug}/manifest) for offline verification."}}}}}}}}},"401":{"description":"401 policy_denied — not signed in (owner routes take only `Authorization: Bearer <Privy token>`; cookies never work).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{id}/submit":{"post":{"tags":["listings"],"operationId":"submitListing","x-sato-contract":"listings/v1","security":[{"bearerAuth":[]}],"summary":"Submit a draft for first-party review","description":"Owner only. draft → submitted. Content is frozen from here on; a change is a new draft.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Submitted.","content":{"application/json":{"schema":{"type":"object","description":"The owner's view of their own listing. Owner routes only.","required":["id","slug","state","project_id","version_id","license","acquisition_modes","row_version","preview"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"state":{"type":"string","enum":["draft","submitted","published","rejected","suspended","unpublished"]},"project_id":{"type":"string","format":"uuid"},"version_id":{"type":"string","format":"uuid"},"license":{"type":"string"},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"public_fields":{"type":"object","additionalProperties":true},"owner_fields":{"type":"object","additionalProperties":true},"review_reason":{"type":["string","null"]},"suspend_reason":{"type":["string","null"]},"submitted_at":{"type":["string","null"]},"published_at":{"type":["string","null"]},"unpublished_at":{"type":["string","null"]},"suspended_at":{"type":["string","null"]},"superseded_by":{"type":["string","null"]},"row_version":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"preview":{"type":"object","description":"The public view of a published listing. Never carries an account id, project id, version id, session id, customer parameters, a storage key or a signed URL.","required":["slug","state","title","task","description","creator","version","recipe","inputs","outputs","runtime","required_services","required_env_names","model_support","operating_cost_basis","license","license_source","fork_permitted","support","attribution","screenshots","evidence_freshness","integrity","acquisition_modes","published_at","sato_url","manifest_url"],"properties":{"slug":{"type":"string"},"state":{"type":"string","enum":["published"]},"title":{"type":"string"},"task":{"type":"string"},"description":{"type":"string"},"creator":{"type":"object","properties":{"display_name":{"type":["string","null"]}}},"version":{"type":"object","properties":{"digest":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"sealed_at":{"type":"string","format":"date-time"},"schema_version":{"type":"integer"},"signing":{"type":"string","enum":["signed","unavailable"]},"artifact_bytes":{"type":"integer"}}},"recipe":{"type":"object","properties":{"id":{"type":["string","null"]},"version":{"type":["string","null"]},"revision":{"type":["string","null"]},"title":{"type":["string","null"]},"summary":{"type":["string","null"]}}},"inputs":{"description":"The version's input JSON schema."},"outputs":{"description":"The version's output JSON schema."},"runtime":{"type":"object","additionalProperties":true},"required_services":{"type":"object","properties":{"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}}}},"required_env_names":{"type":["array","null"],"items":{"type":"string"},"description":"Names only, and only when the owner opted in."},"model_support":{"type":"object","properties":{"policy":{"type":["object","null"],"additionalProperties":true},"label":{"type":"string","enum":["configured, not evaluated"]}}},"operating_cost_basis":{"type":["string","null"]},"license":{"type":"string","description":"SPDX id chosen at publish."},"license_source":{"type":"string","enum":["declared","owner_choice"]},"fork_permitted":{"type":"boolean"},"support":{"type":["string","null"]},"attribution":{"type":["string","null"]},"screenshots":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string"}}}},"evidence_freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"integrity":{"type":"string","enum":["ok","stale_evidence","unchecked"]},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"published_at":{"type":["string","null"],"format":"date-time"},"sato_url":{"type":"string","format":"uri"},"manifest_url":{"type":"string","format":"uri","description":"The public signed release manifest (GET /api/listings/{slug}/manifest) for offline verification."}}}}}}}},"401":{"description":"401 policy_denied — not signed in (owner routes take only `Authorization: Bearer <Privy token>`; cookies never work).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — refused by policy: a body field the server derives, a secret-scan hit in an owner-entered field, a license that does not permit fork, or a state transition that is not allowed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"409":{"description":"409 — the slug belongs to another account or is already live, or the listing changed state since you read it.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/listings/{id}/unpublish":{"post":{"tags":["listings"],"operationId":"unpublishListing","x-sato-contract":"listings/v1","security":[{"bearerAuth":[]}],"summary":"Unpublish (or withdraw) a listing","description":"Owner only. draft | submitted | published | suspended → unpublished. The public page and API answer 404 on the next request.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Unpublished.","content":{"application/json":{"schema":{"type":"object","description":"The owner's view of their own listing. Owner routes only.","required":["id","slug","state","project_id","version_id","license","acquisition_modes","row_version","preview"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"state":{"type":"string","enum":["draft","submitted","published","rejected","suspended","unpublished"]},"project_id":{"type":"string","format":"uuid"},"version_id":{"type":"string","format":"uuid"},"license":{"type":"string"},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"public_fields":{"type":"object","additionalProperties":true},"owner_fields":{"type":"object","additionalProperties":true},"review_reason":{"type":["string","null"]},"suspend_reason":{"type":["string","null"]},"submitted_at":{"type":["string","null"]},"published_at":{"type":["string","null"]},"unpublished_at":{"type":["string","null"]},"suspended_at":{"type":["string","null"]},"superseded_by":{"type":["string","null"]},"row_version":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"preview":{"type":"object","description":"The public view of a published listing. Never carries an account id, project id, version id, session id, customer parameters, a storage key or a signed URL.","required":["slug","state","title","task","description","creator","version","recipe","inputs","outputs","runtime","required_services","required_env_names","model_support","operating_cost_basis","license","license_source","fork_permitted","support","attribution","screenshots","evidence_freshness","integrity","acquisition_modes","published_at","sato_url","manifest_url"],"properties":{"slug":{"type":"string"},"state":{"type":"string","enum":["published"]},"title":{"type":"string"},"task":{"type":"string"},"description":{"type":"string"},"creator":{"type":"object","properties":{"display_name":{"type":["string","null"]}}},"version":{"type":"object","properties":{"digest":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"sealed_at":{"type":"string","format":"date-time"},"schema_version":{"type":"integer"},"signing":{"type":"string","enum":["signed","unavailable"]},"artifact_bytes":{"type":"integer"}}},"recipe":{"type":"object","properties":{"id":{"type":["string","null"]},"version":{"type":["string","null"]},"revision":{"type":["string","null"]},"title":{"type":["string","null"]},"summary":{"type":["string","null"]}}},"inputs":{"description":"The version's input JSON schema."},"outputs":{"description":"The version's output JSON schema."},"runtime":{"type":"object","additionalProperties":true},"required_services":{"type":"object","properties":{"capabilities":{"type":"array","items":{}},"requested_permissions":{"type":"array","items":{}}}},"required_env_names":{"type":["array","null"],"items":{"type":"string"},"description":"Names only, and only when the owner opted in."},"model_support":{"type":"object","properties":{"policy":{"type":["object","null"],"additionalProperties":true},"label":{"type":"string","enum":["configured, not evaluated"]}}},"operating_cost_basis":{"type":["string","null"]},"license":{"type":"string","description":"SPDX id chosen at publish."},"license_source":{"type":"string","enum":["declared","owner_choice"]},"fork_permitted":{"type":"boolean"},"support":{"type":["string","null"]},"attribution":{"type":["string","null"]},"screenshots":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"alt":{"type":"string"}}}},"evidence_freshness":{"type":"object","required":["status","count","last_observed_at","labels"],"properties":{"status":{"type":"string","enum":["not_checked","checked","expired"]},"count":{"type":"integer","description":"Real evidence rows for THIS digest."},"last_observed_at":{"type":["string","null"],"format":"date-time"},"labels":{"type":"array","items":{"type":"string"}}}},"integrity":{"type":"string","enum":["ok","stale_evidence","unchecked"]},"acquisition_modes":{"type":"array","items":{"type":"string","enum":["download","fork"]}},"published_at":{"type":["string","null"],"format":"date-time"},"sato_url":{"type":"string","format":"uri"},"manifest_url":{"type":"string","format":"uri","description":"The public signed release manifest (GET /api/listings/{slug}/manifest) for offline verification."}}}}}}}},"401":{"description":"401 policy_denied — not signed in (owner routes take only `Authorization: Bearer <Privy token>`; cookies never work).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"403":{"description":"403 — refused by policy: a body field the server derives, a secret-scan hit in an owner-entered field, a license that does not permit fork, or a state transition that is not allowed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}},"404":{"description":"404 — no such listing, not published, not yours, or the feature is disabled. One answer for all four, so a prober cannot tell them apart.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["unsupported","missing_requirement","policy_denied","conflict","not_found","stale_evidence"]},"detail":{"type":"string"}}}}}}}}},"/api/x402/history/{slug}":{"get":{"tags":["x402"],"operationId":"getPaidScoreHistory","summary":"Daily Sato Score history for one listing (x402, paid)","description":"Daily Sato Score history for one Sato Hub listing: score, tier components, GitHub stars, status and verification state per day, with the dates the score moved (genuine changes vs our own method revisions marked). Captured daily since 2026-06-12; not reconstructable. The current catalog and the daily score line stay free. Unknown, deprecated or unscored slugs are answered (404/400) before any payment is requested, so nobody pays for nothing.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0.005"},"protocols":[{"x402":{}}]},"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"example":"coinbase-agentkit","description":"A listing slug from the free catalog at /api/export/index.json."},{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":1},"example":90,"description":"How many daily points to return (bounded server-side)."}],"responses":{"200":{"description":"The per-day record for the listing.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`bad_slug` — malformed slug (never charged)."},"402":{"description":"Payment Required — an x402 v2 PAYMENT-REQUIRED challenge (USDC). Also answered, with `error: payment_invalid` and the facilitator's reason, when a presented payment is rejected (never charged)."},"404":{"description":"Unknown, deprecated or unscored listing, or no history yet (never charged)."},"502":{"description":"`facilitator_error` — the payment facilitator could not be reached or failed; nothing was charged, retry later."}}}},"/api/x402/agent-economy":{"get":{"tags":["x402"],"operationId":"getPaidAgentEconomySeries","summary":"Weekly agent-economy series (x402, paid)","description":"The weekly agent-economy series behind Sato Hub's free current-week counts: one row per venue, chain, stage and week, measured from public chain reads. Rows are never summed across venues, chains or stages; null means unknown, never zero. The current week stays free at /api/export/adoption.json.","x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0.005"},"protocols":[{"x402":{}}]},"parameters":[{"name":"weeks","in":"query","required":false,"schema":{"type":"integer","minimum":1},"example":12,"description":"How many weekly points to return (bounded server-side)."}],"responses":{"200":{"description":"The weekly series.","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required — an x402 v2 PAYMENT-REQUIRED challenge (USDC). Also answered, with `error: payment_invalid` and the facilitator's reason, when a presented payment is rejected (never charged)."},"404":{"description":"`no_rows` / `no_history` — nothing matches the filters (never charged)."},"502":{"description":"`facilitator_error` — the payment facilitator could not be reached or failed; nothing was charged, retry later."}}}},"/api/agents/{slug}/manifest":{"get":{"tags":["registry"],"operationId":"getAgentManifest","summary":"An agent's passport manifest","description":"The machine-readable `sato.agent.manifest/v1` document for one listed agent.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The manifest.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No listed agent with that slug."}}}}}}