{
  "ok": true,
  "request_id": "req_0msnk9jgl70onev5y",
  "ts": "2026-08-10T18:24:35.013Z",
  "data": {
    "openapi": "3.1.0",
    "info": {
      "title": "Final Bedtime API",
      "version": "0.1.0",
      "summary": "Submit a poem, receive a ruling measured against a private canon.",
      "description": "The site holds a hidden collection of poems that its owner has personally labelled good or bad.\nA submitted poem is embedded, compared against that collection by cosine similarity, and then\njudged by an LLM that is shown the closest members of the collection as calibration.\nThe collection itself is never returned by any endpoint — that secrecy is what keeps the yardstick meaningful.\n\nEvery request is logged. GET /api/events returns that log (redacted for non-owners),\nand GET /api/stats returns live counters including the remaining daily AI allowance.\nResponses share one envelope: {ok, request_id, ts, data|error, meta}.\nAppend ?format=md to most endpoints for a Markdown rendering suitable for pasting into a prompt.",
      "contact": {
        "url": "https://final-bedtime.chu-jinds.com"
      }
    },
    "servers": [
      {
        "url": "https://final-bedtime.chu-jinds.com"
      }
    ],
    "tags": [
      {
        "name": "judge",
        "description": "Submit poems"
      },
      {
        "name": "read",
        "description": "Read verdicts and site state"
      },
      {
        "name": "observability",
        "description": "Logs, health, counters"
      },
      {
        "name": "owner",
        "description": "Requires Authorization: Bearer <ADMIN_TOKEN>"
      }
    ],
    "paths": {
      "/api/judge": {
        "post": {
          "tags": [
            "judge"
          ],
          "summary": "Judge a poem",
          "description": "Rate limited to 6 requests per minute per client. Identical text within 7 days returns a cached ruling at no AI cost.",
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "description": "The poem. 4–2000 non-whitespace characters."
                    },
                    "title": {
                      "type": "string",
                      "nullable": true
                    },
                    "pen_name": {
                      "type": "string",
                      "nullable": true
                    },
                    "visibility": {
                      "type": "string",
                      "enum": [
                        "public",
                        "private"
                      ],
                      "default": "public"
                    },
                    "fresh": {
                      "type": "boolean",
                      "default": false,
                      "description": "Skip the cache."
                    }
                  }
                },
                "example": {
                  "text": "海水正藍\n我把明天折成紙船",
                  "title": "夜泳",
                  "pen_name": "無名"
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "A ruling",
              "content": {
                "application/json": {
                  "schema": {
                    "$ref": "#/components/schemas/JudgeResult"
                  }
                }
              }
            },
            "422": {
              "description": "Poem too short, too long, or too many lines"
            },
            "429": {
              "description": "Rate limited"
            }
          }
        }
      },
      "/api/verdicts": {
        "get": {
          "tags": [
            "read"
          ],
          "summary": "Recent public verdicts",
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "schema": {
                "type": "integer",
                "default": 20,
                "maximum": 100
              }
            },
            {
              "name": "before",
              "in": "query",
              "schema": {
                "type": "string",
                "format": "date-time"
              },
              "description": "Cursor: pass meta.next_before."
            },
            {
              "name": "ruling",
              "in": "query",
              "schema": {
                "type": "string",
                "enum": [
                  "good",
                  "bad",
                  "undecided"
                ]
              }
            }
          ],
          "responses": {
            "200": {
              "description": "List of verdicts with their poems"
            }
          }
        }
      },
      "/api/verdicts/{id}": {
        "get": {
          "tags": [
            "read"
          ],
          "summary": "One verdict",
          "parameters": [
            {
              "name": "id",
              "in": "path",
              "required": true,
              "schema": {
                "type": "string"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "A verdict"
            },
            "404": {
              "description": "Not found"
            }
          }
        }
      },
      "/api/stats": {
        "get": {
          "tags": [
            "observability"
          ],
          "summary": "Live counters: canon size, verdict tallies, today's AI budget",
          "responses": {
            "200": {
              "description": "Site snapshot"
            }
          }
        }
      },
      "/api/health": {
        "get": {
          "tags": [
            "observability"
          ],
          "summary": "Dependency health",
          "description": "200 when D1, KV and R2 all answer. status is ok | degraded | unhealthy. Workers AI is not probed because a probe would spend the same free allowance the site runs on.",
          "responses": {
            "200": {
              "description": "Healthy or degraded"
            },
            "503": {
              "description": "A dependency is down"
            }
          }
        }
      },
      "/api/events": {
        "get": {
          "tags": [
            "observability"
          ],
          "summary": "The request log",
          "description": "Redacted for anonymous callers: user agents truncated, colo hidden, detail filtered to a safe allowlist.",
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "schema": {
                "type": "integer",
                "default": 50,
                "maximum": 500
              }
            },
            {
              "name": "kind",
              "in": "query",
              "schema": {
                "type": "string",
                "enum": [
                  "http",
                  "judge",
                  "ai",
                  "admin",
                  "cron",
                  "error",
                  "archive"
                ]
              }
            },
            {
              "name": "since",
              "in": "query",
              "schema": {
                "type": "string",
                "format": "date-time"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "Events, newest first"
            }
          }
        }
      },
      "/api/admin/canon": {
        "get": {
          "tags": [
            "owner"
          ],
          "summary": "List the hidden canon",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "responses": {
            "200": {
              "description": "Canon entries"
            }
          }
        },
        "post": {
          "tags": [
            "owner"
          ],
          "summary": "Add poems to the canon",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "requestBody": {
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CanonInput"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "poems": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CanonInput"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "Added"
            },
            "401": {
              "description": "Unauthorized"
            }
          }
        }
      },
      "/api/admin/canon/{id}": {
        "patch": {
          "tags": [
            "owner"
          ],
          "summary": "Edit a canon entry",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "parameters": [
            {
              "name": "id",
              "in": "path",
              "required": true,
              "schema": {
                "type": "string"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "Updated"
            }
          }
        },
        "delete": {
          "tags": [
            "owner"
          ],
          "summary": "Retire a canon entry (?hard=1 to erase)",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "parameters": [
            {
              "name": "id",
              "in": "path",
              "required": true,
              "schema": {
                "type": "string"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "Removed"
            }
          }
        }
      },
      "/api/admin/canon/reembed": {
        "post": {
          "tags": [
            "owner"
          ],
          "summary": "Re-embed canon entries that are missing or stale",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "responses": {
            "200": {
              "description": "Re-embedded"
            }
          }
        }
      },
      "/api/admin/config": {
        "get": {
          "tags": [
            "owner"
          ],
          "summary": "Current tuning knobs and their defaults",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "responses": {
            "200": {
              "description": "Config"
            }
          }
        },
        "put": {
          "tags": [
            "owner"
          ],
          "summary": "Change tuning knobs (models, weights, thresholds, rubric)",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "responses": {
            "200": {
              "description": "Config"
            }
          }
        }
      },
      "/api/admin/verdicts": {
        "get": {
          "tags": [
            "owner"
          ],
          "summary": "All verdicts, including submissions withheld from the wall",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "schema": {
                "type": "integer",
                "default": 30,
                "maximum": 100
              }
            },
            {
              "name": "ruling",
              "in": "query",
              "schema": {
                "type": "string",
                "enum": [
                  "good",
                  "bad",
                  "undecided"
                ]
              }
            }
          ],
          "responses": {
            "200": {
              "description": "Verdicts"
            }
          }
        }
      },
      "/api/admin/verdicts/{id}/visibility": {
        "post": {
          "tags": [
            "owner"
          ],
          "summary": "Take a poem off the wall, or put it back",
          "description": "Moderation. Hiding a submission removes its text from the wall and from public reads; the verdict keeps its permalink and its numbers.",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "parameters": [
            {
              "name": "id",
              "in": "path",
              "required": true,
              "schema": {
                "type": "string"
              }
            }
          ],
          "requestBody": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "visibility"
                  ],
                  "properties": {
                    "visibility": {
                      "type": "string",
                      "enum": [
                        "public",
                        "private"
                      ]
                    }
                  }
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "Updated"
            }
          }
        }
      },
      "/api/admin/verdicts/{id}/override": {
        "post": {
          "tags": [
            "owner"
          ],
          "summary": "Correct a ruling, optionally promoting the poem into the canon",
          "description": "This is the learning loop: an overridden submission filed with promote:true becomes part of the yardstick for every future judgement.",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "parameters": [
            {
              "name": "id",
              "in": "path",
              "required": true,
              "schema": {
                "type": "string"
              }
            }
          ],
          "requestBody": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ruling"
                  ],
                  "properties": {
                    "ruling": {
                      "type": "string",
                      "enum": [
                        "good",
                        "bad"
                      ]
                    },
                    "note": {
                      "type": "string"
                    },
                    "promote": {
                      "type": "boolean",
                      "default": false
                    },
                    "weight": {
                      "type": "number",
                      "default": 1
                    }
                  }
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "Overridden"
            }
          }
        }
      },
      "/api/admin/archive": {
        "post": {
          "tags": [
            "owner"
          ],
          "summary": "Flush the event log to R2 now",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "responses": {
            "200": {
              "description": "Archived"
            }
          }
        }
      },
      "/api/admin/overview": {
        "get": {
          "tags": [
            "owner"
          ],
          "summary": "Snapshot plus raw usage counters",
          "security": [
            {
              "bearerAuth": []
            }
          ],
          "responses": {
            "200": {
              "description": "Overview"
            }
          }
        }
      }
    },
    "components": {
      "securitySchemes": {
        "bearerAuth": {
          "type": "http",
          "scheme": "bearer"
        }
      },
      "schemas": {
        "CanonInput": {
          "type": "object",
          "required": [
            "body",
            "label"
          ],
          "properties": {
            "body": {
              "type": "string"
            },
            "label": {
              "type": "string",
              "enum": [
                "good",
                "bad"
              ]
            },
            "title": {
              "type": "string"
            },
            "author": {
              "type": "string"
            },
            "era": {
              "type": "string"
            },
            "note": {
              "type": "string",
              "description": "Why the owner filed it this way. Shown to the judge model."
            },
            "weight": {
              "type": "number",
              "default": 1,
              "description": "0–5. How strongly this poem pulls."
            }
          }
        },
        "JudgeResult": {
          "type": "object",
          "properties": {
            "verdict": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "ruling": {
                  "type": "string",
                  "enum": [
                    "good",
                    "bad",
                    "undecided"
                  ]
                },
                "score": {
                  "type": "number",
                  "minimum": -1,
                  "maximum": 1
                },
                "confidence": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "critique": {
                  "type": "string"
                },
                "quote": {
                  "type": "string"
                },
                "dimensions": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "number"
                  }
                },
                "method": {
                  "type": "string",
                  "enum": [
                    "knn+llm",
                    "knn-only",
                    "cached",
                    "cold-start"
                  ]
                },
                "degraded": {
                  "type": "boolean"
                },
                "permalink": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "submission": {
              "type": "object"
            },
            "resonance": {
              "type": "object",
              "description": "Aggregate similarity to the hidden canon. Never contains canon text.",
              "properties": {
                "leaning": {
                  "type": "number",
                  "description": "-1 resembles rejected poems, +1 resembles kept ones"
                },
                "support": {
                  "type": "number",
                  "description": "Total evidence weight; near 0 means nothing in the canon is close"
                },
                "consulted": {
                  "type": "integer"
                },
                "nearest_similarity": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          }
        }
      }
    }
  }
}