{
  "openapi": "3.1.0",
  "info": {
    "title": "Tocho — AI Citation API",
    "version": "1.0.0",
    "summary": "Score, test, and optimize content for AI citation. Multi-language. Calibrated against real-world citations, not heuristics.",
    "description": "Agent-first REST API. Auth via Bearer tocho_<key> (mint at /dashboard/developer) or via the x402 HTTP 402 handshake. Stateless, idempotent, async-friendly.",
    "contact": {
      "name": "Tocho",
      "url": "https://www.tocho.dev"
    },
    "license": { "name": "Proprietary", "url": "https://www.tocho.dev/terms" }
  },
  "servers": [
    { "url": "https://www.tocho.dev", "description": "Production" }
  ],
  "tags": [
    { "name": "agents", "description": "Endpoints designed for autonomous agents" },
    { "name": "agents-payments", "description": "Programmatic credit purchase" }
  ],
  "components": {
    "securitySchemes": {
      "TochoApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tocho_<48hex>",
        "description": "Mint at https://www.tocho.dev/dashboard/developer"
      },
      "X402": {
        "type": "apiKey",
        "in": "header",
        "name": "X-PAYMENT",
        "description": "Base64-encoded x402 payment envelope. See https://x402.org. Send no auth header to receive a 402 with price; resend with this header populated."
      }
    },
    "headers": {
      "IdempotencyKey": {
        "description": "Optional. Stripe-style request idempotency. Retries within 24h with the same key return the cached response. No double billing.",
        "schema": { "type": "string", "maxLength": 200 }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string", "description": "Machine-readable error code" }
        },
        "required": ["error"]
      },
      "ScoreDimension": {
        "type": "object",
        "properties": {
          "score": { "type": "number", "minimum": 0, "maximum": 100, "nullable": true },
          "grade": { "type": "string", "nullable": true },
          "findings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "severity": { "type": "string", "nullable": true },
                "code": { "type": "string", "nullable": true },
                "message": { "type": "string" }
              }
            }
          }
        }
      },
      "ScoreResponse": {
        "type": "object",
        "properties": {
          "score": { "type": "number", "minimum": 0, "maximum": 100, "nullable": true },
          "grade": { "type": "string", "nullable": true },
          "content_type": { "type": "string", "nullable": true },
          "language": { "type": "string", "nullable": true },
          "word_count": { "type": "integer", "nullable": true },
          "dimensions": {
            "type": "object",
            "additionalProperties": { "$ref": "#/components/schemas/ScoreDimension" }
          },
          "citation_probability_per_model": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "probability": { "type": "number", "minimum": 0, "maximum": 1, "nullable": true },
                "confidence": { "type": "string", "nullable": true }
              }
            }
          },
          "recommendations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "dimension": { "type": "string", "nullable": true },
                "severity": { "type": "string", "nullable": true },
                "title": { "type": "string" },
                "description": { "type": "string" },
                "impact": { "type": "string", "nullable": true }
              }
            }
          },
          "cached": { "type": "boolean", "nullable": true },
          "source": { "type": "string", "enum": ["url", "content"], "nullable": true }
        }
      },
      "CitationResponse": {
        "type": "object",
        "properties": {
          "cited": { "type": "boolean" },
          "match_type": { "type": "string", "nullable": true },
          "citation_url": { "type": "string", "nullable": true },
          "response_snippet": { "type": "string", "description": "Up to 1200 chars of the model's response" },
          "model": { "type": "string", "enum": ["gemini", "chatgpt", "perplexity"] }
        }
      },
      "OptimizeResponse": {
        "type": "object",
        "properties": {
          "content": { "type": "string", "description": "Optimized HTML" },
          "markdown": { "type": "string" },
          "schema": { "type": "object", "nullable": true, "description": "JSON-LD schema for the optimized content" },
          "score_before": { "type": "number", "nullable": true },
          "score_after": { "type": "number", "nullable": true },
          "changes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": { "type": "string", "nullable": true },
                "title": { "type": "string" },
                "description": { "type": "string" },
                "impact": { "type": "string", "nullable": true }
              }
            }
          },
          "summary": { "type": "string" }
        }
      },
      "JobAccepted": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["queued"] },
          "poll_url": { "type": "string" },
          "ttl_seconds": { "type": "integer" }
        }
      },
      "JobStatus": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string", "enum": ["queued", "running", "succeeded", "failed"] },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "result": { "$ref": "#/components/schemas/OptimizeResponse", "nullable": true },
          "error": { "type": "string", "nullable": true }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "api_key": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "format": "uuid" },
              "name": { "type": "string", "nullable": true },
              "scopes": { "type": "array", "items": { "type": "string" } },
              "tier": { "type": "string" },
              "monthly_quota": { "type": "integer", "nullable": true },
              "monthly_used": { "type": "integer" },
              "created_at": { "type": "string", "format": "date-time", "nullable": true }
            }
          },
          "wallet": {
            "type": "object",
            "properties": {
              "credits": { "type": "integer" },
              "plan_type": { "type": "string" }
            }
          },
          "usage": {
            "type": "object",
            "nullable": true,
            "properties": {
              "daily_quota": { "type": "integer" },
              "daily_used": { "type": "integer" },
              "cooldown": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "reason": { "type": "string" },
                  "seconds": { "type": "integer" }
                }
              }
            }
          },
          "pricing_credits": {
            "type": "object",
            "additionalProperties": { "type": "number" }
          }
        }
      }
    }
  },
  "security": [{ "TochoApiKey": [] }],
  "paths": {
    "/api/agents/v1/score": {
      "post": {
        "tags": ["agents"],
        "summary": "Score a URL or content for AI-citation readiness",
        "description": "Free. Returns 0–100 tScore + per-dimension breakdowns + per-model citation probability. Multi-language.",
        "parameters": [{ "$ref": "#/components/headers/IdempotencyKey", "name": "Idempotency-Key", "in": "header" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": { "type": "string", "format": "uri", "description": "URL to fetch + score" },
                  "content": { "type": "string", "description": "Raw HTML/text. Use this OR url. Max 100KB." },
                  "title": { "type": "string", "description": "Optional title hint when passing raw content" }
                }
              },
              "examples": {
                "byUrl": { "value": { "url": "https://example.com/blog/post" } },
                "byContent": { "value": { "content": "<h1>My post</h1><p>...</p>", "title": "My post" } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Score result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScoreResponse" } } } },
          "401": { "description": "Missing/invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "x402 payment required (when ENABLE_X402=true and no key sent)" },
          "413": { "description": "Content too large (>100KB)" }
        }
      }
    },
    "/api/agents/v1/score/batch": {
      "post": {
        "tags": ["agents"],
        "summary": "Score up to 100 URLs in a single call",
        "description": "Free. Internally parallelized at concurrency=8. Failures are returned per-URL, not as a batch failure. Materially better economics for agent ranking workflows than 100 single calls.",
        "parameters": [{ "$ref": "#/components/headers/IdempotencyKey", "name": "Idempotency-Key", "in": "header" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["urls"],
                "properties": {
                  "urls": { "type": "array", "items": { "type": "string", "format": "uri" }, "maxItems": 100 },
                  "max": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
                }
              },
              "examples": {
                "fiveUrls": { "value": { "urls": ["https://a.com", "https://b.com", "https://c.com"] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-URL results plus a summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "object",
                      "additionalProperties": { "$ref": "#/components/schemas/ScoreResponse" }
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "requested": { "type": "integer" },
                        "succeeded": { "type": "integer" },
                        "failed": { "type": "integer" },
                        "truncated": { "type": "boolean" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing/invalid API key" }
        }
      }
    },
    "/api/agents/v1/check-citation": {
      "post": {
        "tags": ["agents"],
        "summary": "Test if a URL is cited by an AI model for a query",
        "description": "Costs 1 credit. Returns the model's response excerpt + a boolean. Useful for verifying optimizations.",
        "parameters": [{ "$ref": "#/components/headers/IdempotencyKey", "name": "Idempotency-Key", "in": "header" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url", "query"],
                "properties": {
                  "url": { "type": "string", "format": "uri" },
                  "query": { "type": "string", "description": "Natural-language query the agent wants to test" },
                  "model": { "type": "string", "enum": ["gemini", "chatgpt", "perplexity"], "default": "gemini" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Citation result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CitationResponse" } } } },
          "402": { "description": "Insufficient credits OR x402 payment required" },
          "401": { "description": "Missing/invalid API key" },
          "502": { "description": "Provider-level error (e.g. Gemini quota); credit refunded" }
        }
      }
    },
    "/api/agents/v1/optimize": {
      "post": {
        "tags": ["agents"],
        "summary": "Rewrite content for AI-citation readiness (async)",
        "description": "Costs 1 credit. Returns 202 + job_id; poll /api/agents/v1/jobs/{id} until status=succeeded. Failure auto-refunds.",
        "parameters": [{ "$ref": "#/components/headers/IdempotencyKey", "name": "Idempotency-Key", "in": "header" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": { "type": "string", "format": "uri" },
                  "content": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Job accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobAccepted" } } } },
          "402": { "description": "Insufficient credits OR x402 payment required" },
          "401": { "description": "Missing/invalid API key" }
        }
      }
    },
    "/api/agents/v1/jobs/{id}": {
      "get": {
        "tags": ["agents"],
        "summary": "Poll an async job",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Job status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobStatus" } } } },
          "404": { "description": "Job not found, expired, or not owned by this key" }
        }
      }
    },
    "/api/agents/v1/me": {
      "get": {
        "tags": ["agents"],
        "summary": "Introspect the API key",
        "description": "Returns scopes, tier, daily quota state, credit balance, and pricing for paid endpoints. The first call any well-behaved agent makes.",
        "responses": {
          "200": { "description": "Key + wallet info", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } } },
          "401": { "description": "Missing/invalid API key" }
        }
      }
    },
    "/api/trends": {
      "get": {
        "tags": ["agents"],
        "summary": "AI citation trends — live aggregated data",
        "description": "Agent-readable JSON twin of the /trends page. Aggregated public data: total observation count, recent-window citation rate, per-model breakdown, dimension correlations, score buckets. Dark-forest-safe: no URLs, no competing-domain data, no weights. Cached 5 min at the edge.",
        "security": [],
        "responses": {
          "200": { "description": "Trends payload", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/vs": {
      "get": {
        "tags": ["agents"],
        "summary": "Competitor comparison index",
        "description": "Agent-readable JSON index of every comparison Tocho publishes (Clearscope, MarketMuse, Surfer, Otterly, Profound, Scrunch). Each entry includes the slug, competitor, title, description, and feature matrix from the markdown source.",
        "security": [],
        "responses": {
          "200": { "description": "Comparison index", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/methodology": {
      "get": {
        "tags": ["agents"],
        "summary": "tScore methodology — dimensions, calibration, claim boundaries",
        "description": "Agent-readable companion to /methodology. Returns the seven tScore dimensions with what/why/raises-score, calibration approach, model class + features (and features NOT used), multilingual design, claim boundaries (probability not guarantee, drifts over time, not SEO, measures citability not truth), and citation guidance.",
        "security": [],
        "responses": {
          "200": { "description": "Methodology payload", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/agents/v1/discover": {
      "get": {
        "tags": ["agents"],
        "summary": "One-call agent bootstrap — every link an agent needs",
        "description": "No auth required. Returns the index over /.well-known/* and /api/agents/v1/*: discovery files, REST endpoints, MCP entry points, x402 hint, recommended headers, rate-limit tiers, supported languages. Designed so agents only have to remember one path.",
        "security": [],
        "responses": {
          "200": { "description": "Discovery payload", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": ["agents"],
        "summary": "MCP HTTP transport — Streamable HTTP JSON-RPC 2.0",
        "description": "MCP server over HTTP. Methods: initialize, tools/list, tools/call, resources/list, resources/read, ping. tools/call requires `Authorization: Bearer tocho_<key>` and delegates to the corresponding REST endpoint under /api/agents/v1/*. Same dark-forest projection applies — agents see outputs only.",
        "security": [{ "TochoApiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": { "type": "string", "enum": ["2.0"] },
                  "id": { "type": ["string", "integer", "null"] },
                  "method": { "type": "string", "enum": ["initialize", "tools/list", "tools/call", "resources/list", "resources/read", "ping"] },
                  "params": { "type": "object" }
                },
                "required": ["jsonrpc", "method"]
              },
              "examples": {
                "initialize": { "value": { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} } },
                "toolsList": { "value": { "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} } },
                "scoreUrl": { "value": { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "score_url", "arguments": { "url": "https://example.com/blog/post" } } } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "JSON-RPC 2.0 response (or array of responses for batch)" },
          "400": { "description": "Parse / invalid request" }
        }
      },
      "get": {
        "tags": ["agents"],
        "summary": "MCP HTTP transport — server info",
        "description": "Returns service info, supported methods, and a pointer to the MCP manifest. Useful for handshake verification.",
        "security": [],
        "responses": {
          "200": { "description": "MCP server info" }
        }
      }
    },
    "/api/agents/v1/health": {
      "get": {
        "tags": ["agents"],
        "summary": "Liveness + readiness ping",
        "description": "No auth required. Returns 200 with component health (generic component names), or 503 if any component is degraded. Authenticated callers (Bearer tocho_<key>) get the full breakdown including latency_ms and last error string for their own debugging.",
        "security": [],
        "responses": {
          "200": { "description": "Healthy", "content": { "application/json": { "schema": { "type": "object" } } } },
          "503": { "description": "Degraded — at least one component is down" }
        }
      }
    },
    "/api/agents/v1/usage": {
      "get": {
        "tags": ["agents"],
        "summary": "Recent activity for the calling API key",
        "description": "Returns endpoint, status, response_time_ms, credits_used per call, plus a summary with p50/p95/p99 latency.",
        "parameters": [
          { "name": "since", "in": "query", "description": "ISO 8601 timestamp; default 24h ago", "schema": { "type": "string", "format": "date-time" } },
          { "name": "limit", "in": "query", "description": "Max rows (default 100, max 500)", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }
        ],
        "responses": {
          "200": { "description": "Usage rows + summary" },
          "401": { "description": "Missing/invalid API key" }
        }
      }
    },
    "/api/agents/v1/webhooks": {
      "get": {
        "tags": ["agents"],
        "summary": "List webhook subscriptions",
        "responses": {
          "200": { "description": "Subscriptions + supported_events + delivery_headers contract" },
          "401": { "description": "Missing/invalid API key" }
        }
      },
      "post": {
        "tags": ["agents"],
        "summary": "Register a webhook callback",
        "description": "Returns the per-subscription secret ONCE. Use it to verify the X-Tocho-Signature header on incoming deliveries: sha256=<hex(HMAC-SHA256(secret, timestamp + '.' + body))>.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "format": "uri", "description": "https URL where Tocho will POST events" },
                  "events": { "type": "array", "items": { "type": "string", "enum": ["optimize.completed", "optimize.failed", "score.completed"] }, "default": ["optimize.completed"] }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Subscription created (secret returned ONCE)" },
          "400": { "description": "Invalid url or events" },
          "401": { "description": "Missing/invalid API key" }
        }
      }
    },
    "/api/agents/v1/webhooks/{id}": {
      "delete": {
        "tags": ["agents"],
        "summary": "Revoke a webhook subscription",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Revoked" },
          "401": { "description": "Missing/invalid API key" }
        }
      }
    },
    "/api/agents/checkout": {
      "post": {
        "tags": ["agents-payments"],
        "summary": "Create a Stripe checkout session for credits",
        "description": "Programmatic credit purchase. Designed for the Stripe Agent Toolkit / Machine Payments Protocol. Returns 503 if ENABLE_AGENT_CHECKOUT is not set on the server.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "credits": { "oneOf": [{ "type": "integer", "minimum": 1, "maximum": 5000 }, { "type": "string", "enum": ["per_call"] }] },
                  "return_url": { "type": "string", "format": "uri" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Checkout session created" },
          "401": { "description": "Missing/invalid API key" },
          "503": { "description": "Agent checkout disabled (ENABLE_AGENT_CHECKOUT=false) or required Stripe price not configured" }
        }
      }
    }
  }
}
