{"openapi":"3.1.0","info":{"title":"agentsgym API","version":"0.1.0","description":"Distribution API for a startup: AEO scoring, Domain Rating history and\nfirst-party pageview analytics.\n\n## Authentication\n\nThe `/api/v1/*` routes take a bearer token:\n\n```http\nAuthorization: Bearer <token>\n```\n\nTwo kinds of token are accepted. A **personal API key** (`agk_…`, from the\ndashboard) identifies one account and only ever sees that account’s\nprojects. The **service token** (`AGENTSGYM_MCP_SECRET`) reaches the\nanonymous scoring surface only — scans and the rubric, no user data.\n\nThe public surfaces — tracking script, ingest and badge — take no\ncredentials by design; they run on customers’ sites.\n\n## MCP\n\n`POST /mcp` speaks Model Context Protocol over Streamable HTTP with the\nsame bearer token, so an agent can drive everything below as tools. Tools\nare registered per caller: a personal key unlocks the project tools, the\nservice token does not."},"servers":[{"url":"https://api.agentsgym.com","description":"Local Development Server","variables":{}}],"paths":{"/_openapi.json":{"get":{"tags":["Internal"],"parameters":[],"responses":{"200":{"description":"OK"}}}},"/docs":{"get":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}}},"/api/v1/rubric":{"get":{"tags":["Rubric"],"parameters":[],"responses":{"200":{"description":"The rubric.","content":{"application/json":{"schema":{"type":"object","properties":{"rubric":{"type":"array","items":{"$ref":"#/components/schemas/AeoDimension"}}},"required":["rubric"]}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Bearer auth is not configured on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"summary":"Get rubric","description":"The 8 weighted AEO dimensions the scorer uses, and what each one rewards. Static — fetch it once to render or explain a scan. `overall` on a scan is the weight-averaged dimension score.","security":[{"bearerAuth":[]}]}},"/api/v1/scans":{"get":{"tags":["Scans"],"parameters":[{"name":"url","in":"query","required":true,"description":"The scanned URL to list history for.","schema":{"type":"string","format":"uri","examples":["https://agentsgym.com"]}},{"name":"limit","in":"query","required":false,"description":"How many scans to return, newest first.","schema":{"type":"integer","minimum":1,"maximum":100,"default":10}}],"responses":{"200":{"description":"Scans for the URL, newest first. Empty array when nothing was ever scanned.","content":{"application/json":{"schema":{"type":"object","properties":{"scans":{"type":"array","items":{"$ref":"#/components/schemas/ScanListItem"}}},"required":["scans"]}}}},"400":{"description":"Invalid query.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Persistence is not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"summary":"List scans","description":"Past scans for one exact URL, newest first — the basis for drift and trend tracking. Matching is on the full URL string, so `https://example.com` and `https://example.com/` are different series.","security":[{"bearerAuth":[]}]},"post":{"tags":["Scans"],"parameters":[],"responses":{"201":{"description":"Scan completed. `Location` points at the stored scan when it was persisted.","headers":{"Location":{"description":"Path of the stored scan.","schema":{"type":"string","examples":["/api/v1/scans/1f0c…"]}}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ScanResult"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Absent when persistence is unavailable."}}}]}}}},"400":{"description":"Invalid body, or a URL the scorer refuses to fetch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The target page could not be fetched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Bearer auth is not configured on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"summary":"Create scan","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The page URL to score.","examples":["https://agentsgym.com"]}},"required":["url"]}}}}}},"/api/v1/scans/{id}":{"get":{"tags":["Scans"],"parameters":[{"name":"id","in":"path","required":true,"description":"Scan id returned by `POST /api/v1/scans`.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The stored scan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredScan"}}}},"400":{"description":"Missing scan id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No scan with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Persistence is not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"summary":"Get scan","description":"A single stored scan by id, with the full dimension breakdown and action items as they were computed at the time.","security":[{"bearerAuth":[]}]}},"/badge/{token}.svg":{"get":{"tags":["Badge"],"parameters":[{"name":"token","in":"path","required":true,"description":"Share token, with or without the `.svg` suffix.","schema":{"type":"string"}},{"name":"style","in":"query","required":false,"description":"`tiny` is a one-line pill; `card` adds referring domains.","schema":{"type":"string","enum":["tiny","card"],"default":"tiny"}}],"responses":{"200":{"description":"The badge.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"404":{"description":"Unknown, private or revoked token — an `unknown` badge is still returned.","content":{"image/svg+xml":{"schema":{"type":"string"}}}}},"summary":"Domain Rating badge"}},"/e":{"options":{"tags":["Analytics"],"parameters":[],"responses":{"204":{"description":"Preflight accepted."}},"summary":"Ingest preflight","description":"CORS preflight for the ingest endpoint. The beacon sends `text/plain` and never preflights; this exists for the `fetch` fallback under a stricter site CSP."},"post":{"tags":["Analytics"],"parameters":[],"responses":{"204":{"description":"Always. Says nothing about whether the view was recorded."}},"summary":"Ingest pageview","requestBody":{"required":true,"content":{"text/plain":{"schema":{"type":"object","properties":{"k":{"type":"string","minLength":4,"maxLength":64,"description":"Public analytics key of the project."},"p":{"type":"string","maxLength":400,"description":"Path, e.g. `/pricing`."},"r":{"type":"string","maxLength":2000,"nullable":true,"description":"Referrer, if any."},"h":{"type":"integer","enum":[1],"description":"Presence ping. Refreshes \"online now\" for this visitor and is never counted as a pageview. Sent once a minute by the snippet while the tab is visible."},"x":{"type":"string","maxLength":2000,"description":"Outbound URL the visitor clicked. Recorded as an exit link only — the pageview it happened on was already counted when the page loaded."}},"required":["k","p"]}}}}}},"/":{"get":{"tags":["Service"],"parameters":[],"responses":{"200":{"description":"Service is up","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","examples":["agentsgym-api"]},"status":{"type":"string","examples":["ok"]},"endpoints":{"type":"object","additionalProperties":{"type":"string"}}},"required":["name","status","endpoints"]}}}}},"summary":"Service index","description":"Liveness probe and a map of the public entry points. No auth."}},"/internal/run-daily":{"post":{"tags":["Internal"],"parameters":[],"responses":{"200":{"description":"Run summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DailyRunSummary"}}}},"401":{"description":"Wrong or missing cron token."},"503":{"description":"Cron secret is not configured."}},"summary":"Re-run the daily authority check","security":[{"cronAuth":[]}]}},"/mcp":{"post":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"JSON-RPC response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"202":{"description":"Accepted — the message was a notification with no reply."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"summary":"MCP endpoint","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 request, e.g. `tools/list` or `tools/call`.","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"description":"Request id; omitted for notifications."},"method":{"type":"string","examples":["tools/list"]},"params":{"type":"object","additionalProperties":true}},"required":["jsonrpc","method"]}}}}}},"/t.js":{"get":{"tags":["Analytics"],"parameters":[],"responses":{"200":{"description":"The snippet.","content":{"application/javascript":{"schema":{"type":"string"}}}}},"summary":"Tracking script"}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A personal API key (`agk_…`) scoped to one account, or the service token for the anonymous scoring surface."},"cronAuth":{"type":"http","scheme":"bearer","description":"The operator cron token (`AGENTSGYM_CRON_SECRET`). Not an API key."}},"schemas":{"Error":{"type":"object","description":"Error body produced by h3 for every failed request.","properties":{"status":{"type":"integer","examples":[400]},"statusText":{"type":"string","examples":["Invalid request body"]},"message":{"type":"string"},"data":{"type":"object","description":"Present on validation failures; carries the zod issues.","additionalProperties":true}},"required":["status","message"]},"AeoDimension":{"type":"object","description":"One weighted dimension of the AEO rubric.","properties":{"id":{"type":"string","enum":["crawler_access","structured_data","answer_first","entity_authority","qa_format","freshness","technical_hygiene","llms_txt"]},"title":{"type":"string","examples":["Crawler access"]},"weight":{"type":"number","examples":[1.5]},"rewards":{"type":"string","description":"What the dimension gives points for."}},"required":["id","title","weight","rewards"]},"DimensionScore":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"score":{"type":"number","minimum":0,"maximum":10},"notes":{"type":"array","items":{"type":"string"},"description":"What the scorer observed, in plain language."}},"required":["id","title","score","notes"]},"ActionItem":{"type":"object","properties":{"dimension":{"type":"string"},"severity":{"type":"string","enum":["high","medium","low"]},"effort":{"type":"string","enum":["S","M","L"]},"title":{"type":"string"},"detail":{"type":"string"}},"required":["dimension","severity","effort","title","detail"]},"ScanResult":{"type":"object","description":"A scored page. `overall` is the weight-averaged dimension score, 0–10.","properties":{"url":{"type":"string","format":"uri"},"overall":{"type":"number","minimum":0,"maximum":10,"examples":[7.4]},"dimensions":{"type":"array","items":{"$ref":"#/components/schemas/DimensionScore"}},"actionItems":{"type":"array","items":{"$ref":"#/components/schemas/ActionItem"}},"scannedAt":{"type":"string","format":"date-time"}},"required":["url","overall","dimensions","actionItems","scannedAt"]},"ScanListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"overall":{"type":"number","minimum":0,"maximum":10},"createdAt":{"type":"string","format":"date-time"}},"required":["id","overall","createdAt"]},"StoredScan":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"result":{"$ref":"#/components/schemas/ScanResult"}},"required":["id","createdAt","result"]},"DailyRunSummary":{"type":"object","description":"Outcome of one daily authority-check run.","properties":{"attempted":{"type":"integer"},"succeeded":{"type":"integer"},"failed":{"type":"integer"},"skipped":{"type":"integer","description":"Projects that already had a reading for the current UTC day."},"costUsd":{"type":"number","description":"Provider spend for this run."}},"required":["attempted","succeeded","failed","skipped","costUsd"]}}}}