{"components":{"headers":{"RateLimit-Limit":{"description":"Maximum number of requests allowed in the current window (per IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying the request (sent with 429 responses).","schema":{"type":"integer"}}},"schemas":{"CommentRequest":{"additionalProperties":false,"properties":{"author":{"maxLength":100,"minLength":1,"type":"string"},"body":{"maxLength":4000,"minLength":1,"type":"string"},"email":{"format":"email","maxLength":254,"type":"string"},"post":{"description":"Blog post slug such as \"/2026/08/grok-bot\" (leading slash and trailing .html are normalized). Must match a published post.","type":"string"},"website":{"description":"Honeypot field for bots; must be left empty. A non-empty value is silently discarded.","type":"string"}},"required":["post","author","body"],"type":"object"},"ContactRequest":{"additionalProperties":false,"properties":{"email":{"format":"email","maxLength":254,"type":"string"},"message":{"maxLength":8000,"minLength":1,"type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"website":{"description":"Honeypot field for bots; must be left empty. A non-empty value is silently discarded.","type":"string"}},"required":["name","email","message"],"type":"object"},"Error":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"},"GenerateRequest":{"additionalProperties":false,"properties":{"prompt":{"description":"Prompt describing the card to describe, e.g. \"generate description for blog card\". Must mention a supported card type.","type":"string"}},"required":["prompt"],"type":"object"},"GenerateResponse":{"additionalProperties":false,"properties":{"description":{"type":"string"}},"required":["description"],"type":"object"},"Health":{"additionalProperties":false,"properties":{"status":{"type":"string"},"timestamp":{"format":"date-time","type":"string"}},"required":["status","timestamp"],"type":"object"},"JdRequest":{"description":"Job description as raw text or as an https URL; exactly one of text or url must be present.","oneOf":[{"additionalProperties":false,"properties":{"company":{"maxLength":200,"type":"string"},"note":{"maxLength":2000,"type":"string"},"text":{"description":"Job description text, at most 32 KB (UTF-8 bytes).","minLength":1,"type":"string"},"website":{"description":"Honeypot field for bots; must be left empty. A non-empty value is silently discarded.","type":"string"}},"required":["text"],"type":"object"},{"additionalProperties":false,"properties":{"company":{"maxLength":200,"type":"string"},"note":{"maxLength":2000,"type":"string"},"url":{"description":"Link to the job description; must use https.","format":"uri","maxLength":2048,"type":"string"},"website":{"description":"Honeypot field for bots; must be left empty. A non-empty value is silently discarded.","type":"string"}},"required":["url"],"type":"object"}]},"ServiceInfo":{"additionalProperties":false,"properties":{"endpoints":{"additionalProperties":false,"properties":{"cardDescription":{"type":"string"},"comments":{"type":"string"},"contact":{"type":"string"},"health":{"type":"string"},"jd":{"type":"string"}},"required":["health","cardDescription","contact","jd","comments"],"type":"object"},"name":{"type":"string"},"status":{"type":"string"},"version":{"type":"string"}},"required":["name","version","status","endpoints"],"type":"object"},"SubmissionAccepted":{"additionalProperties":false,"properties":{"id":{"format":"uuid","type":"string"},"status":{"enum":["pending"],"type":"string"}},"required":["id","status"],"type":"object"}},"securitySchemes":{"bearerAuth":{"description":"Static API token issued for agents (API_TOKEN / AGENT_API_TOKEN), passed as `Authorization: Bearer <token>`.","scheme":"bearer","type":"http"},"oauth2":{"description":"OAuth 2.0 against the authorization server at https://dynamite-home.pages.dev (see /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server).","flows":{"clientCredentials":{"scopes":{"read:profile":"Read the authenticated agent's public profile information.","chat":"Send prompts to LLM-backed endpoints such as POST /api/llm/generate."},"tokenUrl":"https://dynamite-home.pages.dev/oauth/token"}},"type":"oauth2"}}},"info":{"description":"Public API for the Dynamite site: AI-generated card descriptions and submissions (contact messages, job descriptions, blog comments). All endpoints are rate limited per IP and return RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers on every response; exhausted quotas produce a 429 response with a Retry-After header (seconds until reset). Authentication uses either a static bearer token or OAuth 2.0 scopes published in /.well-known/oauth-protected-resource.","title":"Dynamite API","version":"0.1.0"},"openapi":"3.1.0","paths":{"/":{"get":{"description":"Returns the service name, version, health status, and a map of available endpoints.","operationId":"getServiceInfo","responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"endpoints":{"additionalProperties":false,"properties":{"cardDescription":{"type":"string"},"comments":{"type":"string"},"contact":{"type":"string"},"health":{"type":"string"},"jd":{"type":"string"}},"required":["health","cardDescription","contact","jd","comments"],"type":"object"},"name":{"type":"string"},"status":{"type":"string"},"version":{"type":"string"}},"required":["name","version","status","endpoints"],"type":"object"}}},"description":"Service information.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}}},"security":[],"summary":"Get service info","tags":["Meta"]}},"/api/comments":{"post":{"description":"Submits a blog comment for moderation. The post slug must match a published post (looked up from dynamite-blog.pages.dev/posts-data.json, cached for one hour); unknown slugs return 404. Bodies over 8 KB are rejected. Limited to 5 requests per IP per 10 minutes on this route; the RateLimit-* headers reflect that bucket.","operationId":"submitComment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionAccepted"}}},"description":"Submission accepted and queued for review.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"400":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Malformed JSON, unknown field, or a field outside its documented bounds."},"404":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"The post slug does not match a published post."},"413":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Request body exceeds the size limit."},"415":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Content-Type is not application/json."},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}},"503":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Submission store or upstream post index unavailable."}},"security":[],"summary":"Submit a blog comment","tags":["Submissions"]}},"/api/contact":{"post":{"description":"Submits a contact message. Bodies over 8 KB are rejected. Limited to 5 requests per IP per 10 minutes on this route; the RateLimit-* headers reflect that bucket.","operationId":"submitContact","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionAccepted"}}},"description":"Submission accepted and queued for review.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"400":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Malformed JSON, unknown field, or a field outside its documented bounds."},"413":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Request body exceeds the size limit."},"415":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Content-Type is not application/json."},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}},"503":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Submission store or upstream post index unavailable."}},"security":[],"summary":"Submit a contact message","tags":["Submissions"]}},"/api/jd":{"post":{"description":"Submits a job description either as text (at most 32 KB) or as an https URL. The JSON envelope may be at most 40 KB. Limited to 5 requests per IP per 10 minutes on this route; the RateLimit-* headers reflect that bucket.","operationId":"submitJobDescription","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JdRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionAccepted"}}},"description":"Submission accepted and queued for review.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"400":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Malformed JSON, unknown field, or a field outside its documented bounds."},"413":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Request body exceeds the size limit."},"415":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Content-Type is not application/json."},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}},"503":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Submission store or upstream post index unavailable."}},"security":[],"summary":"Submit a job description","tags":["Submissions"]}},"/api/llm/generate":{"post":{"description":"Generates a short card description with an LLM. Requires authentication and is limited to 10 requests/min/IP on top of the global limit. The prompt must mention a supported card type (\"blog card\" or \"featured posts card\").","operationId":"generateLlmCardDescription","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateResponse"}}},"description":"Generated card description.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"400":{"content":{"application/json":{"schema":{"properties":{"error":{"type":"string"},"supportedTypes":{"items":{"type":"string"},"type":"array"}},"required":["error"],"type":"object"}}},"description":"Missing/invalid prompt or unsupported card type. May include supportedTypes listing accepted card types."},"401":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Missing or invalid bearer token."},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}},"500":{"content":{"application/json":{"schema":{"properties":{"error":{"type":"string"},"fallback":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Upstream generation failed. May include a fallback description."}},"security":[{"oauth2":["chat"]},{"bearerAuth":[]}],"summary":"Generate LLM card description","tags":["LLM"]}},"/health":{"get":{"description":"Liveness probe returning an ok status and the current server time. Used by uptime checks and listed in /.well-known/api-catalog.","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"status":{"type":"string"},"timestamp":{"format":"date-time","type":"string"}},"required":["status","timestamp"],"type":"object"}}},"description":"Service is healthy.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}}},"security":[],"summary":"Health check","tags":["Meta"]}},"/openapi.json":{"get":{"description":"Returns this OpenAPI document. Mirrored statically at https://dynamite-home.pages.dev/openapi.json.","operationId":"getOpenApiDocument","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OpenAPI 3.1 document.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}}},"429":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Rate limit exceeded. The response includes a Retry-After header with the number of seconds until the quota resets.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}}}},"security":[],"summary":"Get OpenAPI document","tags":["Meta"]}}},"servers":[{"url":"https://dynamite-api.tamtn-dynamite-work.workers.dev"},{"url":"https://dynamite-home.pages.dev/api","description":"Same-origin mirror"}]}