{
  "openapi": "3.1.0",
  "info": {
    "title": "Stargazer",
    "summary": "Turn documents, text, or URLs into a spoken audio briefing / podcast episode and deliver it to a private RSS feed that appears in any podcast app.",
    "version": "1.0.0",
    "description": "Turn documents, text, or URLs into a spoken audio briefing / podcast episode and deliver it to a private RSS feed that appears in any podcast app.\n\nTwo ways to buy, one pipeline behind both.\n\n1. A workspace API key spends prepaid credits from a subscription ($25 a month for 25 episodes, packs of 10 for $10). Send `Authorization: Bearer <key>`.\n2. No key, no account, no signup: ask `POST /v1/quotes` for a price (free, unauthenticated), then `POST /v1/episodes?quote=<id>`. Unpaid, that answers 402 with x402 payment requirements in the `PAYMENT-REQUIRED` header. Present a signed payment on `PAYMENT-SIGNATURE` and the episode runs.\n\nPayment settles only AFTER the episode is generated and passes verification. A run that fails costs the buyer nothing: the signed authorization is never presented, its nonce is never spent, and the funds never move.\n\nGeneration is asynchronous. A submit answers 202 with an episodeId; poll `GET /v1/episodes/{episodeId}` every 20 to 30 seconds until status is `done` or `error`. Most episodes finish in a few minutes. The finished episode is delivered to a private RSS feed that works in any podcast app, and the audio is also downloadable from the API.\n\nRefusals are machine-readable everywhere on the quote and payment path: `{error, code, message, field?}` with a stable `code`. A written walkthrough for agents lives at https://stargazer.vector.garden/agents and as a skill file at https://stargazer.vector.garden/agents/skill.md.\n\nAn MCP server wraps this API.It answers at https://stargazer.vector.garden/mcp.",
    "contact": {
      "name": "Vector Garden",
      "url": "https://www.vector.garden",
      "email": "hello@vector.garden"
    },
    "termsOfService": "https://stargazer.vector.garden/legal/terms"
  },
  "servers": [
    {
      "url": "https://stargazer.vector.garden",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "externalDocs": {
    "url": "https://stargazer.vector.garden/agents",
    "description": "Stargazer for agents"
  },
  "tags": [
    {
      "name": "quotes",
      "description": "Price a job before buying it. Free and unauthenticated."
    },
    {
      "name": "episodes",
      "description": "Make one, list them, read one back."
    },
    {
      "name": "workspace",
      "description": "What the presenting key may do."
    },
    {
      "name": "documents",
      "description": "Turn a PDF into text you can submit."
    }
  ],
  "paths": {
    "/v1/quotes": {
      "post": {
        "tags": [
          "quotes"
        ],
        "operationId": "createQuote",
        "summary": "Price a job. Free, no key, no account.",
        "description": "Ask what an episode would cost. Nothing is generated and no capacity is held — a quote is an offer, not a reservation. It stands for 900 seconds (default 900) and is single use. Idempotent inside its window: an identical ask from the same caller returns the offer already standing, with the same id and expiry, and answers 200 instead of 201. This is the only door to the paid path, which is why it takes no key.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Required."
                  },
                  "shape": {
                    "type": "string",
                    "enum": [
                      "conversation",
                      "briefing",
                      "critique",
                      "debate"
                    ],
                    "default": "conversation",
                    "description": "conversation: two voices working through the material. briefing: one voice, essentials only. critique: two voices both skeptical of the document. debate: two voices holding opposite sides, worth it only when the material genuinely has two."
                  },
                  "target_minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 60,
                    "default": 5
                  },
                  "targetMinutes": {
                    "type": "integer",
                    "description": "Accepted as an alias for target_minutes. Both spellings work."
                  },
                  "focus": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "One sentence naming the question the episode should answer. The strongest single lever on what it argues."
                  },
                  "sources": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "items": {
                      "$ref": "#/components/schemas/Source"
                    },
                    "description": "Required. Order matters: citation markers are numbered in the order given, so [S1] is the first source for the life of the episode. All-inline material must total at least 400 characters."
                  },
                  "delivery": {
                    "$ref": "#/components/schemas/Delivery"
                  }
                },
                "required": [
                  "title",
                  "sources"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quote already standing for this exact ask.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "201": {
            "description": "A new quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "400": {
            "description": "A field is wrong, and `code` and `field` name it: E_FIELD_REQUIRED, E_FIELD_TYPE, E_FIELD_RANGE, E_FIELD_TOO_LONG, E_UNKNOWN_SHAPE, E_SOURCE_SCHEME, E_SOURCE_EMPTY, E_SOURCE_THIN, E_TOO_MANY_SOURCES, E_DESCRIPTOR_TOO_LARGE, E_DELIVERY_UNKNOWN, E_FEED_NOT_FOUND, E_FEED_RETIRED, E_INVALID_JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many quotes from this caller. `code` is E_QUOTE_RATE_LIMIT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The deployment cannot price anything right now (`code` E_X402_UNCONFIGURED). A configuration fault on this end, not a problem with the request: retry later, and do not change the body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/episodes": {
      "post": {
        "tags": [
          "episodes"
        ],
        "operationId": "createEpisode",
        "summary": "Make an episode. With a quote, without a key, or both.",
        "description": "Three ways in, one pipeline.\n\n**Redeem a quote with no key (x402).** `POST /v1/episodes?quote=<id>` with no Authorization header. Unpaid, this answers 402 and the `PAYMENT-REQUIRED` header carries the requirements. Sign an EIP-3009 authorization for the quoted amount, network and recipient, put it on `PAYMENT-SIGNATURE`, and repeat the call. The body is optional: omit it and the quoted job runs verbatim. Money moves only after the episode is generated and passes verification.\n\n**Redeem a quote with a key.** Same URL, `Authorization: Bearer <key>`. A prepaid credit funds it and the quote adds single-use semantics and a price echo, nothing else.\n\n**Submit directly with a key.** `POST /v1/episodes` with no `quote` parameter. This is the subscriber path and takes the fuller body below.\n\nIdentical submits dedupe: the same body twice returns the existing episode with `deduped: true` and 200, and spends nothing.",
        "parameters": [
          {
            "name": "quote",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A quote id from POST /v1/quotes. Present means redeem; absent means an ordinary key-authenticated submit."
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "x402Payment": []
          },
          {}
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "On a redeem this is optional and, if sent, must describe the same job the quote priced. On a direct submit `title` plus one of `sources`/`transcript` is the minimum.",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Truncated, never rejected."
                  },
                  "sources": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Source"
                    }
                  },
                  "transcript": {
                    "type": "string",
                    "description": "A speaker-tagged script to voice as-is, instead of writing one. This is the re-voice seam: the transcript is always speaker-tagged and the provider decides the voicing."
                  },
                  "shape": {
                    "type": "string",
                    "enum": [
                      "conversation",
                      "briefing",
                      "critique",
                      "debate"
                    ],
                    "default": "conversation"
                  },
                  "targetMinutes": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Above the workspace ceiling is clamped, not refused. The 202 reports the clamped value."
                  },
                  "focus": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "tone": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "A phrase for how it should sound."
                  },
                  "voices": {
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "premium",
                          "standard"
                        ]
                      },
                      {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        }
                      }
                    ],
                    "description": "The voice grade, or a per-speaker override map. A paid workspace gets premium by default, so most submits send nothing here. GET /v1/workspace names the grade a plain submit gets."
                  },
                  "publish": {
                    "type": "boolean",
                    "default": false,
                    "description": "Publish to the owner's public Chronicle feed as well. Always false on a quoted purchase — delivery to the buyer's own private feed happens regardless, and that is a different thing."
                  },
                  "workspace": {
                    "type": "string",
                    "description": "Admin key only: address another workspace."
                  }
                },
                "required": [
                  "title"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A duplicate of an episode this workspace already has. Nothing was spent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "deduped": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted. The pipeline is running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EpisodeAccepted"
                }
              }
            }
          },
          "400": {
            "description": "A field is wrong. The message names it; nothing was spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No key, on a call that needs one. A redeem with no key gets 402 instead, not this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. On the x402 lane this is the challenge: read the `PAYMENT-REQUIRED` header. On the key lane it means the account is out of credits (`E_NO_CREDITS`) or its card is failing (`E_PAST_DUE`) — a person has to act, so never loop on it.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "description": "Base64 JSON: the x402 v2 challenge. This header is the protocol.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PaymentRequired"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "E_NOT_ACTIVE, E_FEED_RETIRED, E_VOICES_UNAVAILABLE, E_FEED_NOT_YOURS, or a workspace mismatch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No quote with that id (E_QUOTE_NOT_FOUND).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The quote was already redeemed (E_QUOTE_CONSUMED) or the body describes a different job than the one priced (E_QUOTE_DESCRIPTOR_MISMATCH).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The quote expired (E_QUOTE_EXPIRED). Ask for a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "A document, or the inline total, is over the workspace cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Either the workspace's daily episode limit, which resets when the UTC day does, or E_X402_RATE_LIMIT: too many KEYLESS redeems from one caller. Presenting a workspace key raises the second one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "E_X402_DISABLED (the pay-per-call rail is off on this deployment; a workspace key still works), E_X402_UNCONFIGURED, E_X402_FACILITATOR_UNAVAILABLE (could not reach the facilitator, retry), or E_X402_LEDGER_UNAVAILABLE (the payment could not be recorded; nothing was charged, so retry is safe).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-x402": {
          "version": 2,
          "scheme": "exact",
          "maxTimeoutSeconds": 21600,
          "settlement": "after-success",
          "enabled": false,
          "note": "A Stargazer vendor extension, not a standard. The authority is the 402 response's PAYMENT-REQUIRED header, which is built per quote."
        }
      },
      "get": {
        "tags": [
          "episodes"
        ],
        "operationId": "listEpisodes",
        "summary": "The 25 most recent episodes this key can see.",
        "responses": {
          "200": {
            "description": "Newest first, scoped to the presenting key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "workspace": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "queued",
                              "running",
                              "done",
                              "error"
                            ]
                          },
                          "duration_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "published_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "voices": {
                            "type": "string",
                            "enum": [
                              "premium",
                              "standard"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/episodes/{episodeId}": {
      "get": {
        "tags": [
          "episodes"
        ],
        "operationId": "getEpisode",
        "summary": "One episode: the stored row and the live pipeline status.",
        "description": "Shaped differently from the submit: the row sits under `episode`, beside `workflow`. Poll `episode.status` and nothing else — queued, running, then done or error. `workflow` is null once the run's instance has aged out; the row is the record.",
        "parameters": [
          {
            "name": "episodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The episode.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episode": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "running",
                            "done",
                            "error"
                          ]
                        },
                        "duration_ms": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "voices": {
                          "type": "string",
                          "description": "The grade that actually spoke, which can differ from the one asked for."
                        }
                      }
                    },
                    "workflow": {
                      "type": [
                        "object",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such episode, or it belongs to another workspace. The two answer alike on purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/episodes/{episodeId}/audio": {
      "get": {
        "tags": [
          "episodes"
        ],
        "operationId": "getEpisodeAudio",
        "summary": "The finished MP3.",
        "description": "64 kbps mono MP3, Range-aware. HEAD works here and is the cheap existence check. The same audio also reaches any podcast app through the private RSS feed, which needs no key.",
        "parameters": [
          {
            "name": "episodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The audio.",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "A byte range."
          },
          "401": {
            "description": "No key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such episode, not yours, or the audio is not written yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/episodes/{episodeId}/transcript": {
      "get": {
        "tags": [
          "episodes"
        ],
        "operationId": "getEpisodeTranscript",
        "summary": "The script it read from.",
        "description": "Plain text, not JSON. Each turn is wrapped in a speaker tag with citation markers inline. Split on the opening tags: the writer occasionally closes a turn with the other speaker's tag and the pipeline tolerates that deliberately, so a strict XML parser will fail on episodes that are otherwise fine.",
        "parameters": [
          {
            "name": "episodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such episode, not yours, or no transcript yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspace": {
      "get": {
        "tags": [
          "workspace"
        ],
        "operationId": "getWorkspace",
        "summary": "Caps, voice grade, usage today, credits.",
        "description": "Read this once before spending anything. Every number here is otherwise only discoverable by bouncing off a refusal.",
        "responses": {
          "200": {
            "description": "What this key can do.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "401": {
            "description": "No key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A workspace key asking about another workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/extract": {
      "post": {
        "tags": [
          "documents"
        ],
        "operationId": "extractPdf",
        "summary": "PDF bytes in, text out. Stateless.",
        "description": "Nothing is stored. Submit the returned text as an ordinary `{text}` source. A scanned PDF with no text layer answers 422, not 400: the file is fine, it just has no words in it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/pdf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The extracted text.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "chars": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "text",
                    "totalPages",
                    "chars"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Not a PDF, or a corrupt one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Over the size ceiling.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A scanned PDF: well-formed, but it carries no text layer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The daily extraction limit for this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A workspace API key. A subscriber creates one on the account page; it spends that account's prepaid credits and can never buy any."
      },
      "x402Payment": {
        "type": "apiKey",
        "in": "header",
        "name": "PAYMENT-SIGNATURE",
        "description": "A base64 x402 v2 payment payload signed against the requirements in the 402 response's PAYMENT-REQUIRED header. The legacy v1 header X-PAYMENT is still accepted."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every machine-readable refusal on this rail. `error` repeats `message` because the older /v1 routes have always carried `error` and existing clients read it.",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Stable. Branch on this, never on the prose."
          },
          "field": {
            "type": "string",
            "description": "Which field was wrong, as a dotted path, e.g. `sources[0].url`."
          }
        },
        "required": [
          "error"
        ]
      },
      "Source": {
        "description": "What the episode is grounded in. A URL to read, or text you already hold.",
        "anyOf": [
          {
            "type": "string",
            "format": "uri",
            "description": "An http(s) URL."
          },
          {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              }
            },
            "required": [
              "url"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "text": {
                "type": "string",
                "minLength": 1
              },
              "label": {
                "type": "string",
                "maxLength": 200,
                "description": "Optional. What this source is called — the name listed in the finished episode's show notes and beside its [S1] citation marker. A URL source is listed by its URL; inline text with no label is listed as \"pasted text\"."
              }
            },
            "required": [
              "text"
            ],
            "additionalProperties": false
          }
        ]
      },
      "Delivery": {
        "type": "object",
        "description": "Where the finished episode appears. `new` means the feed belonging to whoever redeems the quote — their key's feed, or a feed bound to the paying address. `existing` names one by slug, and possession of the slug is re-checked against the redeemer.",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "new",
              "existing"
            ]
          },
          "feed": {
            "type": "string",
            "maxLength": 128,
            "description": "Required when mode is \"existing\"."
          }
        },
        "required": [
          "mode"
        ]
      },
      "Quote": {
        "type": "object",
        "description": "A self-describing offer. Everything needed to complete the purchase without reading a doc page: what it covers, what it costs, when it stops being true, and the exact call that spends it.",
        "properties": {
          "quote_id": {
            "type": "string",
            "examples": [
              "q_4f1c0b2a9d8e7f6a5b4c3d2e1f0a9b8c"
            ]
          },
          "price": {
            "type": "object",
            "properties": {
              "amount": {
                "type": "string",
                "description": "Atomic units of `asset`. USDC is 6-decimal. Never a float."
              },
              "asset": {
                "type": "string",
                "description": "Token contract address."
              },
              "network": {
                "type": "string",
                "description": "CAIP-2, e.g. eip155:8453."
              },
              "display": {
                "type": "string",
                "description": "The same amount as a decimal string, for logs and humans. Never parse it."
              },
              "items": {
                "type": "array",
                "description": "Itemized. One line today; a second line would be data, not a break.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "quantity": {
                      "type": "number"
                    },
                    "amount": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "required": [
              "amount",
              "asset",
              "network",
              "display",
              "items"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "job": {
            "$ref": "#/components/schemas/JobDescriptor"
          },
          "execute": {
            "type": "object",
            "description": "The whole request that redeems this quote, composed for you.",
            "properties": {
              "method": {
                "type": "string",
                "const": "POST"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "auth": {
                "type": "array",
                "description": "The two lanes through one door, in the order they are tried: present a key and a credit funds it; present none and the x402 challenge answers.",
                "items": {
                  "type": "object"
                }
              },
              "body": {
                "type": "string"
              }
            },
            "required": [
              "method",
              "url",
              "auth"
            ]
          }
        },
        "required": [
          "quote_id",
          "price",
          "expires_at",
          "job",
          "execute"
        ]
      },
      "JobDescriptor": {
        "type": "object",
        "description": "The job a quote covers, echoed back inside the quote as `job`.",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "shape": {
            "type": "string",
            "enum": [
              "conversation",
              "briefing",
              "critique",
              "debate"
            ]
          },
          "target_minutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60
          },
          "focus": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Source"
            }
          },
          "delivery": {
            "$ref": "#/components/schemas/Delivery"
          }
        },
        "required": [
          "title",
          "shape",
          "target_minutes",
          "focus",
          "sources",
          "delivery"
        ]
      },
      "PaymentRequired": {
        "type": "object",
        "description": "The x402 v2 challenge. It rides in the `PAYMENT-REQUIRED` response header as base64 JSON — that header IS the protocol — and the same object is repeated in the body as a courtesy so a person with curl can read the price. Clients MUST read the header.",
        "properties": {
          "x402Version": {
            "type": "integer",
            "const": 2
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "E_X402_* — why this attempt was not accepted."
          },
          "resource": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "mimeType": {
                "type": "string"
              },
              "serviceName": {
                "type": "string"
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "accepts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "scheme": {
                  "type": "string",
                  "const": "exact"
                },
                "network": {
                  "type": "string"
                },
                "amount": {
                  "type": "string"
                },
                "asset": {
                  "type": "string"
                },
                "payTo": {
                  "type": "string"
                },
                "maxTimeoutSeconds": {
                  "type": "integer",
                  "description": "The window the server needs, verify through settle. Sign `validBefore` at least this far ahead: settlement happens after generation finishes, so a shorter authorization is refused up front rather than failing later, once the episode exists and nothing can be done about it."
                },
                "extra": {
                  "type": "object",
                  "description": "The token's EIP-712 domain, which the payer signs over.",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "extensions": {
            "type": "object",
            "description": "x402 v2 extensions. `bazaar` carries this service's discovery listing; echo it back in the payment payload."
          }
        },
        "required": [
          "x402Version",
          "accepts"
        ]
      },
      "EpisodeAccepted": {
        "type": "object",
        "description": "The pipeline started. Poll `GET /v1/episodes/{episodeId}` from here.",
        "properties": {
          "episodeId": {
            "type": "string",
            "examples": [
              "ep-20260822T143000-1a2b3c4d"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued"
            ]
          },
          "using": {
            "type": "object",
            "description": "What the submit RESOLVED to, not what it said. An unknown field is accepted and ignored, so this block is the only way to see that a steer landed. `targetMinutes` is the value after the workspace clamp.",
            "properties": {
              "shape": {
                "type": "string",
                "enum": [
                  "conversation",
                  "briefing",
                  "critique",
                  "debate"
                ]
              },
              "targetMinutes": {
                "type": "integer"
              },
              "voices": {
                "type": "string",
                "enum": [
                  "premium",
                  "standard"
                ]
              }
            }
          },
          "balance": {
            "type": "object",
            "description": "Present only when a subscription credit funded the submit."
          },
          "payment": {
            "type": "object",
            "description": "Present only on the x402 lane. `verified` means the money is authorized but has NOT moved; it settles after the episode is generated and passes verification.",
            "properties": {
              "id": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "verified"
                ]
              },
              "amount": {
                "type": "string"
              },
              "asset": {
                "type": "string"
              },
              "network": {
                "type": "string"
              },
              "settles": {
                "type": "string"
              }
            }
          },
          "feed": {
            "type": "object",
            "description": "Present only on the x402 lane: the private feed this payer's episodes appear in. The slug is the credential — anyone holding this URL can play every episode on it.",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "slug": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "episodeId",
          "status"
        ]
      },
      "Workspace": {
        "type": "object",
        "description": "What this key can do, answered before the refusals answer it.",
        "properties": {
          "workspace": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          },
          "voices": {
            "type": "string",
            "enum": [
              "premium",
              "standard"
            ],
            "description": "The grade a submit with no `voices` field gets."
          },
          "shapes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "conversation",
                "briefing",
                "critique",
                "debate"
              ]
            }
          },
          "episodesToday": {
            "type": "object",
            "description": "`used` counts attempts, which is what the daily reservation counts.",
            "properties": {
              "used": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "maxTargetMinutes": {
                "type": "integer"
              },
              "maxDocs": {
                "type": "integer"
              },
              "maxDocChars": {
                "type": "integer"
              },
              "maxTotalChars": {
                "type": "integer"
              },
              "minSourceChars": {
                "type": "integer",
                "examples": [
                  400
                ]
              },
              "maxTitleChars": {
                "type": "integer"
              },
              "maxFocusChars": {
                "type": "integer"
              },
              "maxToneChars": {
                "type": "integer"
              }
            }
          },
          "feedUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Path to this workspace's private feed. A credential: do not log or forward it."
          },
          "credits": {
            "type": "object",
            "description": "Present only on a workspace backed by a subscription."
          }
        },
        "required": [
          "workspace",
          "plan",
          "voices",
          "shapes",
          "episodesToday",
          "limits"
        ]
      }
    }
  },
  "x-error-codes": [
    "E_INVALID_JSON",
    "E_FIELD_REQUIRED",
    "E_FIELD_TYPE",
    "E_FIELD_RANGE",
    "E_FIELD_TOO_LONG",
    "E_UNKNOWN_SHAPE",
    "E_SOURCE_SCHEME",
    "E_SOURCE_EMPTY",
    "E_SOURCE_THIN",
    "E_TOO_MANY_SOURCES",
    "E_DESCRIPTOR_TOO_LARGE",
    "E_DELIVERY_UNKNOWN",
    "E_FEED_NOT_FOUND",
    "E_FEED_RETIRED",
    "E_FEED_NOT_YOURS",
    "E_QUOTE_RATE_LIMIT",
    "E_QUOTE_NOT_FOUND",
    "E_QUOTE_EXPIRED",
    "E_QUOTE_CONSUMED",
    "E_QUOTE_DESCRIPTOR_MISMATCH",
    "E_X402_DISABLED",
    "E_X402_UNCONFIGURED"
  ]
}