{"openapi":"3.1.0","info":{"title":"KeeperHub","version":"1.0.0","description":"Web3 workflow automation platform. Workflows are callable by AI agents via REST or MCP.","x-guidance":"KeeperHub exposes workflows as REST endpoints under /api/mcp/workflows/{slug}/call.\nEach workflow has a slug, accepts a JSON body, and returns a JSON response with `executionId`, `status`, and `output`.\nAuth: paid workflows return HTTP 402 with x402/MPP payment info on the first call; pay (e.g. via agentcash, openclaw, or any x402 client) and replay. Free workflows can be called directly.\nCategories of workflows include: DeFi yield/risk reads (e.g. usdc-yield-rates-aave-vs-compound, aave-v3-health-check, defi-risk-snapshot), tipping primitives (microtip), and write-type workflows that return unsigned calldata for the caller to sign and broadcast.\n\n## Worked examples\n\n### Example 1: Compare USDC yield (paid, $0.01)\nPOST /api/mcp/workflows/usdc-yield-rates-aave-vs-compound/call\nBody: {}\nResponse (after payment): { executionId, status: \"success\", output: { result: { rates: [...], bestRate, bestProtocol } } }\n\n### Example 2: Aave v3 health check (paid, $0.01)\nPOST /api/mcp/workflows/aave-v3-health-check/call\nBody: { \"address\": \"0x...\" }\nResponse (after payment): { executionId, status: \"success\", output: { result: { healthFactor, totalCollateralUSD, totalDebtUSD, riskLevel } } }\n\n### Example 3: Discover available workflows (free)\nGET /api/mcp/workflows  (returns the list of all listed workflows + pricing)\nGET /openapi.json       (this document; full schema for every workflow)\n\nWhen in doubt, fetch /openapi.json and read the per-workflow `x-payment-info`, `security`, and `requestBody` fields.\n\n## Errors, versioning, and rate limits\n\nEvery failure returns the same envelope: `{ error, detail, hint?, docs?, request_id }`. Branch on `error` (a stable snake_case code) and never on `detail`. See `components.schemas.Error` and `x-error-model`.\nPin a call to a version with the `KeeperHub-Version: 1` request header; omit it to track the current version. Breaking changes ship as a new version, announced with `Deprecation` and `Sunset` headers at least 180 days apart. See `x-api-versioning`.\nLimited endpoints return `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` (delta seconds), plus the legacy `X-RateLimit-*` spellings, and `Retry-After` on a 429. Self-throttle from those rather than backing off blindly."},"x-api-versioning":{"current":"1","strategy":"header","header":"KeeperHub-Version","supported":["1"],"policy":"Additive changes (new endpoints, new optional fields, new enum members) ship inside the current version. Breaking changes ship as a new version and never in place.","deprecation":{"headers":["Deprecation","Sunset","Link"],"deprecationHeader":"RFC 9745. Present once an endpoint, a version, or one accepted request shape is deprecated; carries the date the deprecation took effect as a Structured Fields Date - an \"@\" sigil followed by integer seconds since the Unix epoch, e.g. \"@1789516800\". Not an HTTP-date; Sunset is.","sunsetHeader":"RFC 8594. The earliest date the deprecated thing may stop being accepted, as an HTTP-date. Where an endpoint or a version is deprecated, that is the date it may stop answering. Where only one accepted request shape is deprecated, the endpoint keeps answering and the shape stops being accepted; the Link target says which case applies.","linkHeader":"Link: <url>; rel=\"deprecation\" points at the migration note.","minimumNoticeDays":180}},"x-error-model":{"mediaType":"application/json","schema":"#/components/schemas/Error","codeField":"error","messageField":"detail","correlationField":"request_id","correlationHeader":"x-request-id","codes":["unauthorized","insufficient_scope","not_found","invalid_input","conflict","rate_limited","internal_error"]},"x-rate-limits":{"headers":["RateLimit-Limit","RateLimit-Remaining","RateLimit-Reset","X-RateLimit-Limit","X-RateLimit-Remaining","X-RateLimit-Reset"],"retryAfterHeader":"Retry-After","pollIntervalHeader":"X-Poll-Interval-Hint","documented":"https://docs.keeperhub.com/api/errors"},"x-service-info":{"categories":["web3","automation","blockchain"],"docs":{"homepage":"https://docs.keeperhub.com"},"developerPortal":"https://docs.keeperhub.com/platform-reference","errors":"https://docs.keeperhub.com/api/errors","mcp":{"card":"https://keeper.38-242-233-235.sslip.io/.well-known/mcp.json","endpoint":"https://keeper.38-242-233-235.sslip.io/mcp","publicEndpoint":"https://keeper.38-242-233-235.sslip.io/mcp/public","transport":"streamable-http"},"cli":{"name":"kh","install":"brew install keeperhub/tap/kh","docs":"https://docs.keeperhub.com/cli/quickstart"},"sandbox":{"testnets":[{"name":"Ethereum Sepolia","chainId":11155111},{"name":"Base Sepolia","chainId":84532}],"simulation":"Direct-execution tools accept simulate: true, which reports whether a transaction would revert without broadcasting it. EVM chains only.","docs":"https://docs.keeperhub.com/platform-reference"}},"servers":[{"url":"https://keeper.38-242-233-235.sslip.io"}],"components":{"schemas":{"Error":{"type":"object","required":["error","detail"],"properties":{"error":{"type":"string","description":"Stable snake_case error code. Branch on this value; it does not change across releases.","examples":["rate_limited"]},"detail":{"type":"string","description":"Human-readable explanation. Wording is not stable - never parse it."},"hint":{"type":"string","description":"Suggested next step for the caller, when one exists."},"docs":{"type":"string","format":"uri","description":"Deep link to the relevant documentation, when one exists."},"request_id":{"type":"string","description":"Correlation id, echoed on the x-request-id response header. Quote it in support requests."}}}},"headers":{"RateLimit-Limit":{"description":"Requests permitted in the current window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Legacy spelling of RateLimit-Limit.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Legacy spelling of RateLimit-Remaining.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Legacy spelling. Unix epoch seconds at which the window resets - an absolute time, not a delta.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"parameters":{"KeeperHubVersion":{"name":"KeeperHub-Version","in":"header","required":false,"description":"Pins the request to a major version of the REST surface. Omit to use the current version. A breaking change ships as a new version rather than in place, so a pinned caller keeps its behaviour.","schema":{"type":"string","enum":["1"],"default":"1"}}},"responses":{"InvalidInput":{"description":"The request body or query failed validation. `error` is `invalid_input`.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"The credential is missing, malformed, or revoked. `error` is `unauthorized`.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InsufficientScope":{"description":"The credential is valid but not permitted for this operation. `error` is `insufficient_scope`.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The route or the resource does not exist. `error` is `not_found`.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"The request contradicts current state, for example a reused idempotency key with a different body. `error` is `conflict`.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"A published rate limit was exceeded. `error` is `rate_limited`. Wait for `Retry-After` seconds before retrying.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}},"RateLimit-Limit":{"description":"Requests permitted in the current window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Legacy spelling of RateLimit-Limit.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Legacy spelling of RateLimit-Remaining.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Legacy spelling. Unix epoch seconds at which the window resets - an absolute time, not a delta.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"An unexpected server fault. `error` is `internal_error`. Retry with backoff and quote `request_id` if it persists.","headers":{"x-request-id":{"description":"Correlation id, matching request_id in the body.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"securitySchemes":{"x402":{"type":"http","scheme":"Payment","description":"x402 challenge-response payment. The first unpaid call returns HTTP 402 with payment requirements; the client signs that payment and replays the request."},"siwx":{"type":"http","scheme":"bearer","bearerFormat":"CAIP-122","description":"Sign-In with X identity proof (CAIP-122) for compatible agent clients. KeeperHub workflow calls primarily use x402 payment discovery today."},"bearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key (prefix `kh_`) or an OAuth access token, sent as `Authorization: Bearer <token>`. Create an organization key from the API Keys screen; verify it with GET /api/keys."}}},"paths":{"/api/health":{"get":{"operationId":"get-health","summary":"Service health","description":"Liveness probe. Answers without authentication and without touching the database.","security":[],"parameters":[{"name":"KeeperHub-Version","in":"header","required":false,"description":"Pins the request to a major version of the REST surface. Omit to use the current version. A breaking change ships as a new version rather than in place, so a pinned caller keeps its behaviour.","schema":{"type":"string","enum":["1"],"default":"1"}}],"responses":{"200":{"description":"The service is up.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/chains":{"get":{"operationId":"list-chains","summary":"Supported chains","description":"The live source of truth for supported chains, including each chain's support status. Public: answers whether or not a credential is supplied, so it tests reachability rather than a credential.","security":[],"parameters":[{"name":"KeeperHub-Version","in":"header","required":false,"description":"Pins the request to a major version of the REST surface. Omit to use the current version. A breaking change ships as a new version rather than in place, so a pinned caller keeps its behaviour.","schema":{"type":"string","enum":["1"],"default":"1"}}],"responses":{"200":{"description":"Supported chains and their status.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/mcp/workflows":{"get":{"operationId":"list-listed-workflows","summary":"Discover callable workflows","description":"Every listed marketplace workflow with its slug, price, and input schema. Enumerate slugs from here rather than guessing call paths.","security":[],"parameters":[{"name":"KeeperHub-Version","in":"header","required":false,"description":"Pins the request to a major version of the REST surface. Omit to use the current version. A breaking change ships as a new version rather than in place, so a pinned caller keeps its behaviour.","schema":{"type":"string","enum":["1"],"default":"1"}}],"responses":{"200":{"description":"Listed workflows with their pricing and input schemas.","headers":{"RateLimit-Limit":{"description":"Requests permitted in the current window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Legacy spelling of RateLimit-Limit.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Legacy spelling of RateLimit-Remaining.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Legacy spelling. Unix epoch seconds at which the window resets - an absolute time, not a delta.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/keys":{"get":{"operationId":"verify-credential","summary":"Verify an organization API key","description":"The auth probe. 200 means the credential is valid and scoped to an organization; 401 means it is not. Point first-run scripts and health checks here.","security":[{"bearerAuth":[]}],"parameters":[{"name":"KeeperHub-Version","in":"header","required":false,"description":"Pins the request to a major version of the REST surface. Omit to use the current version. A breaking change ships as a new version rather than in place, so a pinned caller keeps its behaviour.","schema":{"type":"string","enum":["1"],"default":"1"}}],"responses":{"200":{"description":"The credential is valid.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}