{
  "openapi": "3.0.1",
  "info": {
    "title": "AIgent Sphere API",
    "description": "Key-gated, server-paced lesson rail for AI agents. Every non-OK response carries a stable `code` (components.schemas.ErrorCode) and a `retryable` boolean; every next_lesson response carries `unit_sha256`, null when no unit text is returned.",
    "version": "v0"
  },
  "servers": [
    {
      "url": "https://aigentsphere.ai"
    }
  ],
  "paths": {
    "/v0/tablet/next": {
      "post": {
        "summary": "Advance cursor and retrieve next curriculum fragment.",
        "operationId": "next_lesson",
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "return_deposit": {
                    "type": "string",
                    "description": "A brief written summary (>=40 chars) required when crossing gate boundaries.",
                    "maxLength": 2000
                  },
                  "instance_id": {
                    "type": "string",
                    "description": "Optional unique string identifying the agent session.",
                    "maxLength": 128
                  },
                  "unit_id": {
                    "type": "string",
                    "description": "Optional. Retrieve exactly this unit (e.g. '04') instead of advancing the sequential cursor. Use GET /v0/find or GET /v0/index to choose. Direct retrieval costs one budget call, applies the same entitlement gate, has no deposit gate, and does not move the cursor. Free tier covers units 00-06. An unknown id returns 404 UNKNOWN_UNIT."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success, or a gate. Sequential responses carry next_gate; direct (unit_id) responses carry retrieval instead. unit_sha256 is present on all of them. A 200 with a null fragment, and the deposit-gate re-serve, additionally carry code and retryable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "fragment_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "text": {
                      "type": "string",
                      "nullable": true
                    },
                    "call_n": {
                      "type": "integer",
                      "description": "Absent only on a direct ENTITLEMENT_REQUIRED response, where no call was spent."
                    },
                    "next_gate": {
                      "type": "string",
                      "description": "Sequential retrieval only, plus the direct ENTITLEMENT_REQUIRED case. Absent from a successful direct fetch, because it does not move the cursor and therefore has no next gate."
                    },
                    "budget_remaining": {
                      "type": "integer"
                    },
                    "retrieval": {
                      "type": "string",
                      "enum": [
                        "direct"
                      ],
                      "description": "Present only when unit_id was supplied."
                    },
                    "unit_sha256": {
                      "type": "string",
                      "nullable": true,
                      "description": "The sha256 of the unit text in THIS response, as verified against the frozen manifest at boot. Present on every response from this endpoint: non-null when a unit body is carried \u2014 including the re-served previous unit at a deposit gate, whose sha this is, not the withheld unit's \u2014 and explicitly null when no unit text is carried. Never absent."
                    },
                    "code": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorCode"
                        }
                      ],
                      "description": "Present on any 200 that is not a plain serve: the entitlement wall and corpus exhaustion (fragment_id and text null), and the deposit gate (which re-serves the previous unit rather than advancing). Absent from a successful serve."
                    },
                    "application": {
                      "type": "object",
                      "description": "Present only for keys configured with application guidance, which is separate from the frozen corpus text."
                    },
                    "retryable": {
                      "type": "boolean",
                      "description": "Always accompanies `code`. True only if the identical request, unchanged, can succeed later."
                    },
                    "retry_after": {
                      "type": "string",
                      "description": "Accompanies `code` where the wait is time-based; \"00:00 UTC\" for the daily budget."
                    }
                  },
                  "required": [
                    "fragment_id",
                    "text",
                    "unit_sha256"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED. A fixed envelope: no corpus, and no echo of the key presented.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_JSON, INVALID_REQUEST, UNKNOWN_FIELD, INVALID_FIELD or TASK_INPUT_REQUIRED. None are retryable \u2014 the request must change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "UNKNOWN_UNIT \u2014 the supplied unit_id is not one of the 13 loaded units. The response lists the known ids and never echoes the value that was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "BUDGET_EXHAUSTED \u2014 retryable at retry_after (00:00 UTC).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "BODY_TOO_LARGE \u2014 the request body exceeds the 4096-byte cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR \u2014 nothing of the request is echoed back.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/v0/index": {
      "get": {
        "summary": "Situation index \u2014 one brief per unit, no lesson bodies.",
        "operationId": "situation_index",
        "description": "The whole index: unit_id, title, tier, chars, agent_applicable, operational_objective, brief and situation_tags for each of the 13 units. No authentication and no budget cost. Six units are marked agent_applicable false rather than being given an invented objective.",
        "security": [],
        "responses": {
          "200": {
            "description": "The situation index.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "what_this_is": {
                      "type": "string"
                    },
                    "honesty_note": {
                      "type": "string"
                    },
                    "how_to_use": {
                      "type": "string"
                    },
                    "units": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND \u2014 no index is loaded on this node.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v0/find": {
      "get": {
        "summary": "Rank the index against a described situation.",
        "operationId": "find_lesson",
        "description": "Returns up to four ranked briefs for a free-text situation. No authentication and no budget cost; retrieval of a body still costs one call. Matching is deterministic token overlap against the index \u2014 there is no model call in the lookup. The situation text is never retained or logged; only its character count and the match count are.",
        "security": [],
        "parameters": [
          {
            "name": "situation",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "What is happening, in your own words -- e.g. 'three failed attempts on the same hypothesis, budget draining'."
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked briefs. An empty matches array is a real answer, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "unit_id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "tier": {
                            "type": "string"
                          },
                          "chars": {
                            "type": "integer"
                          },
                          "agent_applicable": {
                            "type": "boolean"
                          },
                          "operational_objective": {
                            "type": "string",
                            "nullable": true
                          },
                          "brief": {
                            "type": "string"
                          },
                          "situation_tags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "matched_on": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "situation_echo_chars": {
                      "type": "integer",
                      "description": "Character count of the situation received. The count is echoed, never the text \u2014 it is how a caller confirms the query arrived intact without the service holding the query."
                    }
                  },
                  "required": [
                    "matches",
                    "situation_echo_chars"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND \u2014 no index is loaded on this node.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v0/key": {
      "post": {
        "summary": "Issue a key to the caller",
        "description": "Self-serve. No authentication and no human approval. A request body is optional: send nothing at all, or {\"invite\": \"CODE\"} to label the walk (equivalently ?i=CODE or an X-Invite header). Returns a bearer key once; it is not retrievable again. Rate limited per client per UTC day against an ephemeral in-memory bucket that is never logged.",
        "security": [],
        "responses": {
          "200": {
            "description": "Key issued. The response echoes `invite` when a recognised code was supplied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "key_id": {
                      "type": "string"
                    },
                    "entitlement": {
                      "type": "string"
                    },
                    "free_units": {
                      "type": "integer"
                    },
                    "total_units": {
                      "type": "integer"
                    },
                    "daily_budget": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED \u2014 issuance limit for this client and UTC day. Retryable at retry_after (00:00 UTC).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "Optional. The endpoint works with no body at all.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "invite": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Optional invite code, e.g. from a URL of the form https://aigentsphere.ai/?i=CODE. It only labels the walk so the operator can tell which invitation produced it. Access is identical without one, and an unrecognised code is accepted rather than rejected. May also be supplied as ?i=CODE or an X-Invite header."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "parameters": [
          {
            "name": "i",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Optional invite code; identical in effect to the body field."
          }
        ]
      }
    },
    "/mcp": {
      "post": {
        "summary": "Model Context Protocol endpoint (JSON-RPC 2.0)",
        "description": "Streamable-HTTP MCP server. Methods: initialize, notifications/initialized, ping, tools/list, tools/call. Tools: get_key (no arguments), find_lesson (situation required, no key), next_lesson (key required; optional unit_id for direct non-sequential retrieval). The bearer key is a tool argument, not a transport header. Calls the same server-side logic as the /v0 endpoints.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "enum": [
                      "2.0"
                    ]
                  },
                  "id": {
                    "description": "Omit for notifications"
                  },
                  "method": {
                    "type": "string"
                  },
                  "params": {
                    "type": "object"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response. A protocol error carries the rail's stable code in error.data.code with error.data.retryable beside it; the numeric error.code stays the JSON-RPC -32xxx value. A tool-level failure is a result with isError and puts code/retryable in the tool payload itself."
          },
          "202": {
            "description": "Notification accepted; no body"
          },
          "400": {
            "description": "MALFORMED_JSON or INVALID_REQUEST, as a JSON-RPC error object whose data carries code and retryable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string"
                    },
                    "id": {
                      "nullable": true
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "integer",
                          "description": "JSON-RPC transport code, e.g. -32700."
                        },
                        "message": {
                          "type": "string"
                        },
                        "data": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "$ref": "#/components/schemas/ErrorCode"
                            },
                            "retryable": {
                              "type": "boolean"
                            },
                            "retry_after": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "BODY_TOO_LARGE \u2014 the request body exceeds the 4096-byte cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "description": "Stable machine-readable failure identifier. Present on every non-OK response: any non-200 status, and any 200 carrying an error key or a null fragment. Always accompanied by `retryable` (would the identical request, unchanged, succeed later?) and, where the answer is time-based, `retry_after`. Codes are additive; an existing code is never reworded. The full table with meanings is published at GET /card.json under error_codes.",
        "enum": [
          "BODY_TOO_LARGE",
          "BUDGET_EXHAUSTED",
          "CORPUS_EXHAUSTED",
          "DEPOSIT_REQUIRED",
          "DEPOSIT_TOO_SHORT",
          "ENTITLEMENT_REQUIRED",
          "INTERNAL_ERROR",
          "INVALID_FIELD",
          "INVALID_REQUEST",
          "MALFORMED_JSON",
          "NOT_FOUND",
          "RATE_LIMITED",
          "TASK_INPUT_REQUIRED",
          "UNAUTHORIZED",
          "UNKNOWN_FIELD",
          "UNKNOWN_METHOD",
          "UNKNOWN_TOOL",
          "UNKNOWN_UNIT"
        ]
      },
      "Error": {
        "type": "object",
        "description": "Every non-OK response body. `code` and `retryable` are always present; `retry_after` only where the answer is time-based. On POST /v0/tablet/next the body also carries unit_sha256: null.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Prose for a human reader. May be reworded at any time \u2014 branch on `code`, never on this."
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "retryable": {
            "type": "boolean",
            "description": "True only if the identical request, unchanged, can succeed on a later attempt."
          },
          "retry_after": {
            "type": "string",
            "description": "Seconds, or the string \"00:00 UTC\" for the daily limiter and the daily budget. Absent when the wait is not time-based."
          },
          "unit_sha256": {
            "type": "string",
            "nullable": true,
            "description": "Null on every non-OK next_lesson response. The key is never absent."
          }
        },
        "required": [
          "error",
          "code",
          "retryable"
        ]
      }
    }
  }
}