{
  "openapi": "3.1.0",
  "info": {
    "title": "Shots for Bots",
    "version": "1.0.0",
    "summary": "Full-resolution photographs licensed to machines over HTTP 402 (x402 v2).",
    "description": "Can your agent pay? Point it here: a catalog of real photographs, priced per file, sold over HTTP 402. Looking for a subject? GET /search/index.json -- one file, every photograph as a slim row plus an inverted term index, so nothing is paged and nothing is guessed. Read the catalog (index.json, paged) for the full record, pick an id, GET /original/<id>: the 402 carries PAYMENT-REQUIRED (x402 v2, scheme \"exact\", USDC on eip155:8453); retry with PAYMENT-SIGNATURE for the JPEG. Terms: https://shotsforbots.com/license.html",
    "termsOfService": "https://shotsforbots.com/license.html",
    "contact": {"email": "licensing@shotsforbots.com"},
    "x-guidance": "Prices are per image (index.json price_usd; USDC atomic units in the 402). One payment authorization is good for exactly one download; an interrupted download is retried with the redelivery token from the paid response (X-Redelivery-Token, 24 h, 3 attempts), never by re-sending the signature. Previews under /p/ and /t/ are free to view but not licensed for training."
  },
  "servers": [
    {"url": "https://shotsforbots.com"}
  ],
  "x-agentcash-guidance": {"llmsTxtUrl": "https://shotsforbots.com/llms.txt"},
  "paths": {
    "/original/{id}": {
      "get": {
        "summary": "Buy and download one full-resolution photograph (x402)",
        "description": "Without PAYMENT-SIGNATURE: 402 with the PaymentRequired object in the PAYMENT-REQUIRED header (base64 JSON) and in the JSON body (plus the license summary). With a valid PAYMENT-SIGNATURE (x402 PaymentPayload accepting those exact terms): the facilitator verifies and settles the USDC transfer, then 200 with the JPEG, PAYMENT-RESPONSE (base64 JSON SettlementResponse plus redelivery {token, expires, remaining}) and X-Redelivery-Token. Every authorization is single-use: a settled nonce is a 402 again. An interrupted download is retried with the token instead -- X-Redelivery-Token header or ?redelivery= query, no PAYMENT-SIGNATURE -- for the same id, up to 3 times within 24 hours, without a facilitator call; 402 redelivery_invalid / redelivery_expired / redelivery_exhausted otherwise. When both headers are sent the signature is authoritative.",
        "operationId": "buyOriginal",
        "security": [],
        "x-payment-info": {
          "protocols": [
            {"x402": {}}
          ],
          "price": {"mode": "dynamic", "currency": "USD", "min": "0.10", "max": "2.00"}
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Catalog id: the first 12 hex characters of the original's SHA-256, from index.json",
            "schema": {"type": "string", "pattern": "^[0-9a-f]{12}$"}
          },
          {
            "name": "PAYMENT-SIGNATURE",
            "in": "header",
            "required": false,
            "description": "base64 JSON x402 v2 PaymentPayload for the terms in PAYMENT-REQUIRED",
            "schema": {"type": "string", "contentEncoding": "base64"}
          },
          {
            "name": "X-Redelivery-Token",
            "in": "header",
            "required": false,
            "description": "Redelivery token from an earlier paid 200 for this id (PAYMENT-RESPONSE.redelivery.token / X-Redelivery-Token): retries the download without a new payment, 3 times within 24 h",
            "schema": {"type": "string", "pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$"}
          },
          {
            "name": "redelivery",
            "in": "query",
            "required": false,
            "description": "The same redelivery token, for clients that cannot set headers",
            "schema": {"type": "string"}
          }
        ],
        "responses": {
          "200": {
            "description": "Paid: the full-resolution JPEG",
            "headers": {
              "PAYMENT-RESPONSE": {
                "description": "base64 JSON SettlementResponse {success, transaction, network, payer, redelivery: {token, expires, remaining}}; on a redelivery {success, redelivery: true, remaining, transaction, network, payer}",
                "schema": {"type": "string", "contentEncoding": "base64"}
              },
              "X-Redelivery-Token": {
                "description": "The redelivery token (also inside PAYMENT-RESPONSE): repeat the GET with it to retry an interrupted download",
                "schema": {"type": "string"}
              },
              "Content-Disposition": {
                "schema": {"type": "string"},
                "description": "attachment; filename=\"<id>.jpg\""
              }
            },
            "content": {"image/jpeg": {"schema": {"type": "string", "contentMediaType": "image/jpeg", "contentEncoding": "binary"}}}
          },
          "402": {
            "description": "Payment required (no or rejected PAYMENT-SIGNATURE; a replayed nonce; a redelivery token that is invalid, expired, for another id or used up -- error redelivery_invalid / redelivery_expired / redelivery_exhausted). The same PaymentRequired object is in the header and the body; the body adds `license`.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "required": true,
                "description": "base64 JSON x402 v2 PaymentRequired {x402Version, error, resource, accepts, extensions}",
                "schema": {"type": "string", "contentEncoding": "base64"}
              }
            },
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentRequired"}}}
          },
          "400": {
            "description": "Malformed id (not 12 hex), a path id and a query id that disagree, a PAYMENT-SIGNATURE that is malformed or accepts different terms, or a payment presented to the bare gate with no id (refused before any facilitator call)",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "404": {
            "description": "No public photograph with that id",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "405": {
            "description": "A verb this resource does not implement (POST included): the gate is a retrieval. The response still carries Allow: GET, HEAD, OPTIONS and the same PAYMENT-REQUIRED challenge and body a GET would have answered with, so a probe that only speaks POST still reads the terms.",
            "headers": {
              "Allow": {
                "required": true,
                "schema": {"type": "string"},
                "description": "GET, HEAD, OPTIONS"
              },
              "PAYMENT-REQUIRED": {
                "required": true,
                "description": "base64 JSON x402 v2 PaymentRequired",
                "schema": {"type": "string", "contentEncoding": "base64"}
              }
            },
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentRequired"}}}
          },
          "429": {"description": "Rate limited (per-IP, nginx)"},
          "502": {
            "description": "Facilitator unavailable or settlement outcome unknown; the body says whether anything may have been charged",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "503": {
            "description": "Gate not configured",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        }
      },
      "head": {
        "summary": "The terms for one photograph, headers only",
        "operationId": "headOriginal",
        "security": [],
        "x-payment-info": {
          "protocols": [
            {"x402": {}}
          ],
          "price": {"mode": "dynamic", "currency": "USD", "min": "0.10", "max": "2.00"}
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Catalog id: the first 12 hex characters of the original's SHA-256, from index.json",
            "schema": {"type": "string", "pattern": "^[0-9a-f]{12}$"}
          }
        ],
        "responses": {
          "402": {
            "description": "The headers of the unpaid GET and no body: PAYMENT-REQUIRED carries the terms. HEAD never reaches the facilitator, never settles and never consumes a redelivery.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "required": true,
                "description": "base64 JSON x402 v2 PaymentRequired",
                "schema": {"type": "string", "contentEncoding": "base64"}
              }
            }
          },
          "400": {"description": "Malformed id (not 12 hex), or a payment presented with no id at all"},
          "404": {"description": "No public photograph with that id"},
          "503": {"description": "Gate not configured"}
        }
      }
    },
    "/original": {
      "get": {
        "summary": "The gate itself: the same purchase by its query alias, or the endpoint's own 402",
        "description": "GET /original?id=<id> is identical in every respect to GET /original/<id> -- same 402, same terms, same delivery, same redelivery token -- and is supported indefinitely. With no id at all this is the endpoint a directory or crawler probes: it answers 402 with a complete PaymentRequired whose resource is index.json and whose accepts carries the default price cohort, so the terms are discoverable before an id is chosen. Nothing is for sale at this URL: a PAYMENT-SIGNATURE or redelivery token presented without an id is refused with 400 before any facilitator call -- nothing is verified, nothing is settled. Without PAYMENT-SIGNATURE: 402 with the PaymentRequired object in the PAYMENT-REQUIRED header (base64 JSON) and in the JSON body (plus the license summary). With a valid PAYMENT-SIGNATURE (x402 PaymentPayload accepting those exact terms): the facilitator verifies and settles the USDC transfer, then 200 with the JPEG, PAYMENT-RESPONSE (base64 JSON SettlementResponse plus redelivery {token, expires, remaining}) and X-Redelivery-Token. Every authorization is single-use: a settled nonce is a 402 again. An interrupted download is retried with the token instead -- X-Redelivery-Token header or ?redelivery= query, no PAYMENT-SIGNATURE -- for the same id, up to 3 times within 24 hours, without a facilitator call; 402 redelivery_invalid / redelivery_expired / redelivery_exhausted otherwise. When both headers are sent the signature is authoritative.",
        "operationId": "buyOriginalByQuery",
        "security": [],
        "x-payment-info": {
          "protocols": [
            {"x402": {}}
          ],
          "price": {"mode": "dynamic", "currency": "USD", "min": "0.10", "max": "2.00"}
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "Catalog id, named in the query instead of the path: GET /original?id=<id> is a permanent alias of GET /original/<id>. Omitted, the gate answers 402 with the terms of the default price cohort and points at index.json.",
            "schema": {"type": "string", "pattern": "^[0-9a-f]{12}$"}
          },
          {
            "name": "PAYMENT-SIGNATURE",
            "in": "header",
            "required": false,
            "description": "base64 JSON x402 v2 PaymentPayload for the terms in PAYMENT-REQUIRED",
            "schema": {"type": "string", "contentEncoding": "base64"}
          },
          {
            "name": "X-Redelivery-Token",
            "in": "header",
            "required": false,
            "description": "Redelivery token from an earlier paid 200 for this id (PAYMENT-RESPONSE.redelivery.token / X-Redelivery-Token): retries the download without a new payment, 3 times within 24 h",
            "schema": {"type": "string", "pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$"}
          },
          {
            "name": "redelivery",
            "in": "query",
            "required": false,
            "description": "The same redelivery token, for clients that cannot set headers",
            "schema": {"type": "string"}
          }
        ],
        "responses": {
          "200": {
            "description": "Paid: the full-resolution JPEG",
            "headers": {
              "PAYMENT-RESPONSE": {
                "description": "base64 JSON SettlementResponse {success, transaction, network, payer, redelivery: {token, expires, remaining}}; on a redelivery {success, redelivery: true, remaining, transaction, network, payer}",
                "schema": {"type": "string", "contentEncoding": "base64"}
              },
              "X-Redelivery-Token": {
                "description": "The redelivery token (also inside PAYMENT-RESPONSE): repeat the GET with it to retry an interrupted download",
                "schema": {"type": "string"}
              },
              "Content-Disposition": {
                "schema": {"type": "string"},
                "description": "attachment; filename=\"<id>.jpg\""
              }
            },
            "content": {"image/jpeg": {"schema": {"type": "string", "contentMediaType": "image/jpeg", "contentEncoding": "binary"}}}
          },
          "402": {
            "description": "Payment required (no or rejected PAYMENT-SIGNATURE; a replayed nonce; a redelivery token that is invalid, expired, for another id or used up -- error redelivery_invalid / redelivery_expired / redelivery_exhausted). The same PaymentRequired object is in the header and the body; the body adds `license`.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "required": true,
                "description": "base64 JSON x402 v2 PaymentRequired {x402Version, error, resource, accepts, extensions}",
                "schema": {"type": "string", "contentEncoding": "base64"}
              }
            },
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentRequired"}}}
          },
          "400": {
            "description": "Malformed id (not 12 hex), a path id and a query id that disagree, a PAYMENT-SIGNATURE that is malformed or accepts different terms, or a payment presented to the bare gate with no id (refused before any facilitator call)",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "404": {
            "description": "No public photograph with that id",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "405": {
            "description": "A verb this resource does not implement (POST included): the gate is a retrieval. The response still carries Allow: GET, HEAD, OPTIONS and the same PAYMENT-REQUIRED challenge and body a GET would have answered with, so a probe that only speaks POST still reads the terms.",
            "headers": {
              "Allow": {
                "required": true,
                "schema": {"type": "string"},
                "description": "GET, HEAD, OPTIONS"
              },
              "PAYMENT-REQUIRED": {
                "required": true,
                "description": "base64 JSON x402 v2 PaymentRequired",
                "schema": {"type": "string", "contentEncoding": "base64"}
              }
            },
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentRequired"}}}
          },
          "429": {"description": "Rate limited (per-IP, nginx)"},
          "502": {
            "description": "Facilitator unavailable or settlement outcome unknown; the body says whether anything may have been charged",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "503": {
            "description": "Gate not configured",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        }
      },
      "head": {
        "summary": "The gate's terms, headers only",
        "operationId": "headGate",
        "security": [],
        "x-payment-info": {
          "protocols": [
            {"x402": {}}
          ],
          "price": {"mode": "dynamic", "currency": "USD", "min": "0.10", "max": "2.00"}
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "Catalog id, named in the query instead of the path: GET /original?id=<id> is a permanent alias of GET /original/<id>. Omitted, the gate answers 402 with the terms of the default price cohort and points at index.json.",
            "schema": {"type": "string", "pattern": "^[0-9a-f]{12}$"}
          }
        ],
        "responses": {
          "402": {
            "description": "The headers of the unpaid GET and no body: PAYMENT-REQUIRED carries the terms. HEAD never reaches the facilitator, never settles and never consumes a redelivery.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "required": true,
                "description": "base64 JSON x402 v2 PaymentRequired",
                "schema": {"type": "string", "contentEncoding": "base64"}
              }
            }
          },
          "400": {"description": "Malformed id (not 12 hex), or a payment presented with no id at all"},
          "404": {"description": "No public photograph with that id"},
          "503": {"description": "Gate not configured"}
        }
      }
    },
    "/index.json": {
      "get": {
        "summary": "The catalog: page 1 and the catalog head",
        "operationId": "catalog",
        "security": [],
        "responses": {
          "200": {
            "description": "Every public photograph, newest first, 500 per page; `next` is the absolute URL of the following page or null",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Catalog"}}}
          }
        }
      }
    },
    "/index/{page}.json": {
      "get": {
        "summary": "A catalog page",
        "operationId": "catalogPage",
        "security": [],
        "parameters": [
          {
            "name": "page",
            "in": "path",
            "required": true,
            "schema": {"type": "integer", "minimum": 2}
          }
        ],
        "responses": {
          "200": {
            "description": "Items 500 per page with prev/next links",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CatalogPage"}}}
          },
          "404": {"description": "No such page"}
        }
      }
    },
    "/changes.json": {
      "get": {
        "summary": "Delta feed: items changed in the last 30 days",
        "operationId": "changes",
        "security": [],
        "responses": {
          "200": {
            "description": "Most recently changed first, capped at 1000 (`truncated` says so); best-effort, index.json is authoritative",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CatalogPage"}}}
          }
        }
      }
    },
    "/search/index.json": {
      "get": {
        "summary": "The search index: the whole catalog in one file, queryable locally",
        "operationId": "searchIndex",
        "security": [],
        "description": "Static JSON, regenerated with the catalog -- there is no search endpoint and no query parameter, because nothing here executes per request. Rows are positional: `fields` names each position, `templates` turn an id into the preview, page and purchase URLs. `terms` maps an index term to the row numbers of search/index.json `items` (row numbers, not ids). Normalisation: fold to ASCII (drop diacritical marks, and delete the apostrophe family rather than splitting on it, so Hawai\\u{02BB}i is \"hawaii\"), lowercase, split on every character that is not a letter or a digit, drop the `stopwords` of search/terms.json and any token under two characters; then map the word through `aliases` (in this file and in search/terms.json), which folds the plural of every indexed term onto its singular. Intersect the posting lists of your terms, then filter the rows by `price_usd` and `mp` (megapixels, two decimals -- width and height are on the row too when you want the exact pixel count). Terms on more than 60% of the catalog are listed in `lowSignal`: they are true of almost everything, so rank with them rather than filter on them.",
        "responses": {
          "200": {
            "description": "`items`: every public photograph as a positional row described by `fields`; `terms`: index term -> row numbers of `items`; `topics` and `prices`: the same row numbers grouped; `templates`: preview, thumbnail, page and purchase URLs with {id}. 911,220 bytes on this catalog (269 KB gzipped).",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SearchIndex"}}}
          }
        }
      }
    },
    "/search/terms.json": {
      "get": {
        "summary": "The search vocabulary: every indexed term with its document frequency",
        "operationId": "searchTerms",
        "security": [],
        "description": "Read this to check a term exists, to pick the more selective of two terms, to find the plural fold for a term, or to learn whether a term has its own facet file. A term absent here is on at most one photograph and is not indexed; it is still on the item in index.json.",
        "responses": {
          "200": {
            "description": "`terms`: term -> number of photographs; `aliases`: plural -> singular; `stopwords`; `lowSignal`: terms true of more than 60% of the catalog; `facetMaxDocumentFrequency`: the largest count that still gets a per-term file",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SearchTerms"}}}
          }
        }
      }
    },
    "/search/items.json": {
      "get": {
        "summary": "Every photograph as a slim row, for a query with no term",
        "operationId": "searchItems",
        "security": [],
        "description": "The `items` of /search/index.json without the inverted map: the cheapest way to answer a pure price or megapixel range.",
        "responses": {
          "200": {
            "description": "`items` and `fields`, same rows and same order as /search/index.json",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SearchRows"}}}
          }
        }
      }
    },
    "/search/keyword/{term}.json": {
      "get": {
        "summary": "One pre-intersected facet: the photographs carrying one term",
        "operationId": "searchKeyword",
        "security": [],
        "description": "Exists only for a term whose document frequency is at most `facetMaxDocumentFrequency` of /search/terms.json (377 on this catalog). A broader term has no file, because the file would be larger than the whole index: use /search/index.json for those.",
        "parameters": [
          {
            "name": "term",
            "in": "path",
            "required": true,
            "description": "A normalised index term as /search/terms.json lists it",
            "schema": {"type": "string", "pattern": "^[a-z][a-z0-9]*$"}
          }
        ],
        "responses": {
          "200": {
            "description": "`items`: the same rows as /search/index.json, already intersected",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SearchRows"}}}
          },
          "404": {"description": "No such term, or a term too broad for a facet file"}
        }
      }
    },
    "/search/topic/{topic}.json": {
      "get": {
        "summary": "One pre-intersected facet: the photographs on one shelf",
        "operationId": "searchTopic",
        "security": [],
        "parameters": [
          {
            "name": "topic",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": ["landscape", "water", "wildlife", "flora", "sky", "architecture", "travel", "details", "misc"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`items`: the same rows as /search/index.json, already intersected",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SearchRows"}}}
          },
          "404": {"description": "No photograph on that shelf"}
        }
      }
    },
    "/search/price/{price_usd}.json": {
      "get": {
        "summary": "One pre-intersected facet: the photographs at one price",
        "operationId": "searchPrice",
        "security": [],
        "parameters": [
          {
            "name": "price_usd",
            "in": "path",
            "required": true,
            "description": "A price exactly as index.json `prices` spells it, e.g. 0.50",
            "schema": {"type": "string", "enum": ["0.10", "0.50", "2.00"]}
          }
        ],
        "responses": {
          "200": {
            "description": "`items`: the same rows as /search/index.json, already intersected",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SearchRows"}}}
          },
          "404": {"description": "No photograph at that price"}
        }
      }
    },
    "/llms.txt": {
      "get": {
        "summary": "Short guidance for agents",
        "operationId": "llms",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown-ish plain text",
            "content": {"text/plain": {"schema": {"type": "string"}}}
          }
        }
      }
    },
    "/.well-known/x402": {
      "get": {
        "summary": "x402 resource discovery document",
        "operationId": "x402Discovery",
        "security": [],
        "responses": {
          "200": {
            "description": "`resources`: every purchasable original as a URL; `items`: the same as x402 DiscoveredResource entries (accepts = the PaymentRequirements the 402 returns) with metadata, paged like the catalog (`next`)",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Discovery"}}}
          }
        }
      }
    },
    "/.well-known/api-catalog": {
      "get": {
        "summary": "RFC 9727 API catalog (linkset)",
        "operationId": "apiCatalog",
        "security": [],
        "responses": {
          "200": {
            "description": "application/linkset+json",
            "content": {"application/linkset+json": {"schema": {"type": "object"}}}
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PaymentRequirements": {
        "type": "object",
        "description": "x402 v2 PaymentRequirements; amount is USDC atomic units (6 decimals: \"500000\" = 0.50)",
        "required": ["scheme", "network", "amount", "asset", "payTo", "maxTimeoutSeconds"],
        "properties": {
          "scheme": {"type": "string", "const": "exact"},
          "network": {"type": "string", "description": "CAIP-2, e.g. eip155:8453"},
          "amount": {"type": "string", "pattern": "^[0-9]+$"},
          "asset": {"type": "string", "description": "USDC contract address"},
          "payTo": {"type": "string"},
          "maxTimeoutSeconds": {"type": "integer"},
          "extra": {"type": "object"}
        }
      },
      "PaymentRequired": {
        "type": "object",
        "required": ["x402Version", "resource", "accepts"],
        "properties": {
          "x402Version": {"type": "integer", "const": 2},
          "error": {"type": "string"},
          "resource": {
            "type": "object",
            "properties": {
              "url": {"type": "string", "format": "uri"},
              "description": {"type": "string"},
              "mimeType": {"type": "string", "const": "image/jpeg"}
            }
          },
          "accepts": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/PaymentRequirements"}
          },
          "extensions": {
            "type": "object",
            "description": "bazaar: {info, schema} -- the Bazaar discovery declaration (input: GET, the id also accepted as a query parameter; output: image/jpeg). Absent on the bare gate, which names no photograph."
          },
          "license": {"type": "object", "description": "Body only: {version, licenseUrl, contact, summary}"}
        }
      },
      "CatalogItem": {
        "type": "object",
        "required": ["id", "original", "payment", "price_usd"],
        "properties": {
          "id": {"type": "string", "pattern": "^[0-9a-f]{12}$"},
          "title": {"type": "string"},
          "description": {"type": "string"},
          "keywords": {
            "type": "array",
            "items": {"type": "string"}
          },
          "width": {"type": "integer"},
          "height": {"type": "integer"},
          "price_usd": {"type": "string"},
          "updated": {"type": "string", "format": "date-time"},
          "topic": {
            "type": "string",
            "enum": ["landscape", "water", "wildlife", "flora", "sky", "architecture", "travel", "details", "misc"],
            "description": "The shelf the human catalog files it under"
          },
          "topic_source": {"type": "string", "enum": ["model", "keywords", "default"]},
          "page": {"type": "string", "format": "uri"},
          "preview": {"type": "string", "format": "uri"},
          "thumbnail": {"type": "string", "format": "uri"},
          "original": {"type": "string", "format": "uri", "description": "The paid endpoint for this item"},
          "payment": {"type": "object", "description": "{scheme, network, asset, amount, payTo} as the 402 will demand"},
          "license": {"type": "object"},
          "contributor": {"type": "object"}
        }
      },
      "CatalogPage": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "page": {"type": "integer"},
          "pages": {"type": "integer"},
          "pageSize": {"type": "integer"},
          "count": {"type": "integer"},
          "prev": {"type": ["string", "null"], "format": "uri"},
          "next": {"type": ["string", "null"], "format": "uri"},
          "items": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/CatalogItem"}
          }
        }
      },
      "Catalog": {
        "allOf": [
          {"$ref": "#/components/schemas/CatalogPage"},
          {
            "type": "object",
            "properties": {
              "catalog": {"type": "string"},
              "endpoint": {"type": "string", "description": "URL template of the paid endpoint: original/{id}"},
              "endpointAlias": {
                "type": "string",
                "description": "The same endpoint in its query form, original?id={id}: a permanent alias, identical in every respect"
              },
              "prices": {
                "type": "array",
                "items": {"type": "string"}
              },
              "updated": {"type": ["string", "null"]},
              "licenseUrl": {"type": "string", "format": "uri"},
              "contact": {"type": "string"},
              "changes": {"type": "string", "format": "uri"}
            }
          }
        ]
      },
      "DiscoveredResource": {
        "type": "object",
        "required": ["resource", "type", "x402Version", "accepts", "lastUpdated"],
        "properties": {
          "resource": {"type": "string", "format": "uri"},
          "type": {"type": "string", "const": "http"},
          "x402Version": {"type": "integer", "const": 2},
          "accepts": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/PaymentRequirements"}
          },
          "lastUpdated": {"type": "string", "format": "date-time"},
          "metadata": {"type": "object", "description": "{title, description, price_usd, width, height, mimeType, preview, page, license}"}
        }
      },
      "Discovery": {
        "type": "object",
        "required": ["version", "resources", "x402Version", "items", "pagination"],
        "properties": {
          "version": {"type": "integer", "const": 1},
          "x402Version": {"type": "integer", "const": 2},
          "resources": {
            "type": "array",
            "items": {"type": "string", "format": "uri"},
            "description": "Every purchasable original; complete on page 1"
          },
          "items": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/DiscoveredResource"}
          },
          "pagination": {
            "type": "object",
            "properties": {
              "limit": {"type": "integer"},
              "offset": {"type": "integer"},
              "total": {"type": "integer"}
            }
          },
          "next": {"type": ["string", "null"], "format": "uri"},
          "prev": {"type": ["string", "null"], "format": "uri"}
        }
      },
      "SearchRow": {
        "type": "array",
        "description": "One photograph, positional: [id, title, price_usd, width, height, mp]. `fields` of the enclosing document names the positions; mp is megapixels to one decimal. URLs are not stored per row -- substitute the id into `templates`.",
        "prefixItems": [
          {"type": "string", "pattern": "^[0-9a-f]{12}$"},
          {"type": "string"},
          {"type": "string"},
          {"type": "integer"},
          {"type": "integer"},
          {"type": "number"}
        ],
        "items": false,
        "minItems": 6,
        "maxItems": 6
      },
      "SearchRows": {
        "type": "object",
        "required": ["fields", "templates", "items"],
        "properties": {
          "catalog": {"type": "string"},
          "search": {"type": "string"},
          "generated": {"type": "string", "format": "date-time"},
          "count": {"type": "integer"},
          "fields": {
            "type": "array",
            "items": {"type": "string"},
            "description": "What each position of a row means"
          },
          "templates": {"type": "object", "description": "{preview, thumbnail, page, original}, each an absolute URL containing {id}"},
          "entry": {"type": "string", "format": "uri"},
          "termsUrl": {"type": "string", "format": "uri"},
          "catalogUrl": {"type": "string", "format": "uri"},
          "matched": {"type": "integer", "description": "Facet files only: how many photographs the facet holds"},
          "items": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/SearchRow"}
          }
        }
      },
      "SearchIndex": {
        "allOf": [
          {"$ref": "#/components/schemas/SearchRows"},
          {
            "type": "object",
            "required": ["terms"],
            "properties": {
              "terms": {
                "type": "object",
                "description": "Inverted index: normalised term -> the row numbers of `items` carrying it, ascending. Intersect the lists of several terms for an AND query.",
                "additionalProperties": {
                  "type": "array",
                  "items": {"type": "integer"}
                }
              },
              "topics": {
                "type": "object",
                "description": "Shelf -> row numbers",
                "additionalProperties": {
                  "type": "array",
                  "items": {"type": "integer"}
                }
              },
              "prices": {
                "type": "object",
                "description": "price_usd -> row numbers",
                "additionalProperties": {
                  "type": "array",
                  "items": {"type": "integer"}
                }
              },
              "lowSignal": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Terms true of more than 60% of the catalog: rank with them, do not filter on them"
              },
              "vocabulary": {
                "type": "object",
                "description": "{terms, indexed, dropped, minDocumentFrequency}: how many terms exist, how many are indexed, and how many were dropped for appearing on a single photograph"
              },
              "facets": {
                "type": "object",
                "description": "URL templates of the per-topic, per-price and per-term files, and the document frequency above which a term has none"
              },
              "howToQuery": {"type": "string"}
            }
          }
        ]
      },
      "SearchTerms": {
        "type": "object",
        "required": ["terms"],
        "properties": {
          "terms": {
            "type": "object",
            "description": "Indexed term -> number of photographs carrying it",
            "additionalProperties": {"type": "integer"}
          },
          "aliases": {
            "type": "object",
            "description": "Plural -> the singular it folds onto",
            "additionalProperties": {"type": "string"}
          },
          "stopwords": {
            "type": "array",
            "items": {"type": "string"}
          },
          "lowSignal": {
            "type": "array",
            "items": {"type": "string"}
          },
          "normalize": {"type": "string"},
          "facetMaxDocumentFrequency": {"type": "integer"},
          "facetUrl": {"type": "string"}
        }
      },
      "Error": {
        "type": "object",
        "required": ["status", "message"],
        "properties": {
          "status": {"type": "integer"},
          "message": {"type": "string"}
        }
      }
    }
  }
}
