{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "info": {
    "title": "Kaspa News Knowledge Lab API",
    "version": "2026-10-16",
    "summary": "Answers about Kaspa for your own apps.",
    "description": "One endpoint: send a question about Kaspa to `POST /v1/ask` and get back the lab's checked answer, its request id and its exact price. The guide with examples in curl, JavaScript and Python is at https://lab.kaspa.news/docs.",
    "termsOfService": "https://lab.kaspa.news/docs#terms",
    "contact": {
      "name": "Kaspa News",
      "email": "hello@kaspa.news",
      "url": "https://lab.kaspa.news/docs#support"
    }
  },
  "externalDocs": {
    "description": "The guide: quick start, streaming, conversations, errors, limits, pricing and the website chat box",
    "url": "https://lab.kaspa.news/docs"
  },
  "servers": [
    {
      "url": "https://lab-api.kaspa.news",
      "description": "The Kaspa News Knowledge Lab API"
    }
  ],
  "security": [
    {
      "labKey": []
    }
  ],
  "tags": [
    {
      "name": "Answers",
      "description": "Questions about Kaspa and their answers."
    }
  ],
  "paths": {
    "/v1/ask": {
      "post": {
        "tags": ["Answers"],
        "operationId": "ask",
        "summary": "Ask a question about Kaspa",
        "description": "Answers one question about Kaspa. The answer is Markdown with its links inside; the API sends no source list and no source numbers. SilverScript in an answer is compiled with silverc, Python that uses the Kaspa SDK is checked against it, and Unix millisecond timestamps are turned back into dates: the results follow the answer under **Checks**.\n\nWith `\"stream\": true` the answer comes as server-sent events (`token`, then `done` or `error`). A request refused before the answer starts (a bad key, a limit, a bad request) always gets a plain JSON error with its status, also when it asked for a stream.\n\nQuestions that are not about Kaspa, and links to X posts the lab does not have, get one fixed line with `price_usd` 0. Short questions about one live network number are answered from live network data, also for 0.",
        "requestBody": {
          "required": true,
          "description": "One JSON object of at most 128,000 bytes, sent with a Content-Length header. Fields the lab does not know are ignored.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AskRequest"
              },
              "examples": {
                "question": {
                  "summary": "A question",
                  "value": {
                    "question": "What is GHOSTDAG?"
                  }
                },
                "followUp": {
                  "summary": "A follow-up, with the earlier turn",
                  "value": {
                    "question": "How is DAGKnight different?",
                    "history": [
                      {
                        "q": "What is GHOSTDAG?",
                        "a": "GHOSTDAG is the rule Kaspa nodes use to put all blocks into one agreed order, including blocks that were made at the same time."
                      }
                    ]
                  }
                },
                "stream": {
                  "summary": "The answer as it is written",
                  "value": {
                    "question": "How fast is Kaspa?",
                    "stream": true
                  }
                },
                "simple": {
                  "summary": "In very simple words",
                  "value": {
                    "question": "What is a blockDAG?",
                    "simple": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer: a JSON object, or with `\"stream\": true` a stream of server-sent events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                },
                "examples": {
                  "answer": {
                    "summary": "An answer the lab wrote",
                    "value": {
                      "id": "req_9f2c4e1a7b3d5c60",
                      "answer": "**GHOSTDAG** is the rule Kaspa nodes use to put all blocks into one agreed order, including blocks that were made at the same time. Blocks made in parallel are kept instead of thrown away, which lets Kaspa make many blocks every second.",
                      "price_usd": 0.0251
                    }
                  },
                  "fixedLine": {
                    "summary": "A question that is not about Kaspa: one fixed line, free",
                    "value": {
                      "id": "req_95fea651e10a6010",
                      "answer": "I only answer questions about Kaspa. Ask me anything about Kaspa: how it works, wallets, mining or building on it.",
                      "price_usd": 0.0
                    }
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Events of the form `event: <name>`, `data: <JSON>` and an empty line. `token`: data is a JSON string, the next piece of the answer. `done`: data is the Answer object, always the complete, checked answer (the pieces can stop a few characters short of the end). `error`: data is a StreamError; the answer stopped and costs nothing. Lines starting with `:` are keep-alive comments, about every 15 seconds while the answer is being made. Ignore events you do not know."
                },
                "examples": {
                  "stream": {
                    "summary": "A short stream",
                    "value": "event: token\ndata: \"GHOSTDAG is the rule Kaspa nodes use\"\n\nevent: token\ndata: \" to put all blocks into one agreed order\"\n\n: still working\n\nevent: token\ndata: \", including blocks made at the sa\"\n\nevent: done\ndata: {\"id\": \"req_9f2c4e1a7b3d5c60\", \"answer\": \"GHOSTDAG is the rule Kaspa nodes use to put all blocks into one agreed order, including blocks made at the same time.\", \"price_usd\": 0.0251}\n\n"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/LabError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "labKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "klab_ key",
        "description": "An API key from your dashboard at https://lab-dashboard.kaspa.news: `klab_` and 43 more letters, digits, `-` or `_`. Send it as `Authorization: Bearer klab_...`. A key is shown once; keep it on your server."
      }
    },
    "schemas": {
      "AskRequest": {
        "type": "object",
        "required": ["question"],
        "properties": {
          "question": {
            "type": "string",
            "minLength": 1,
            "maxLength": 12000,
            "pattern": "\\S",
            "description": "The question, 1 to 12,000 characters once spaces at both ends are removed. A link to an X post is looked up in the lab's own copy of Kaspa posts and the post is explained.",
            "examples": ["What is GHOSTDAG?"]
          },
          "history": {
            "type": "array",
            "description": "Earlier turns of this conversation, oldest first, for a follow-up. The lab reads the last 3; items that are not objects are skipped.",
            "items": {
              "$ref": "#/components/schemas/Turn"
            },
            "default": []
          },
          "stream": {
            "type": "boolean",
            "default": false,
            "description": "`true` sends the answer as server-sent events while it is written."
          },
          "simple": {
            "type": "boolean",
            "default": false,
            "description": "`true` asks for very simple words, like the ELI5 button on lab.kaspa.news: the short version first, one everyday comparison, every important fact kept exact. Live network numbers and fixed lines stay as they are."
          }
        },
        "additionalProperties": true
      },
      "Turn": {
        "type": "object",
        "description": "One earlier question and the answer the lab gave.",
        "properties": {
          "q": {
            "type": "string",
            "description": "The earlier question. Only its first 1,500 characters are read."
          },
          "a": {
            "type": "string",
            "description": "The lab's answer to it, as you received it. Only its first 4,000 characters are read."
          }
        }
      },
      "Answer": {
        "type": "object",
        "required": ["id", "answer", "price_usd"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^req_[0-9a-f]{16}$",
            "description": "The request id, as in your dashboard's Questions list and monthly statements. Give it when you report a problem.",
            "examples": ["req_9f2c4e1a7b3d5c60"]
          },
          "answer": {
            "type": "string",
            "description": "The answer in Markdown, with links inside the text and no source numbers. Code in it was checked; when there is code or a timestamp, a short **Checks** list follows at the end."
          },
          "price_usd": {
            "type": "number",
            "minimum": 0,
            "description": "What this answer cost in US dollars, exact to 7 decimal places. 0 for fixed lines and live network numbers.",
            "examples": [0.0251]
          }
        },
        "additionalProperties": false
      },
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": {
            "type": "string",
            "description": "What happened, in plain words for people."
          },
          "code": {
            "type": "string",
            "description": "A short word for programs. `bad_request` 400; `invalid_key` 401; `balance_empty`, `monthly_spend_limit`, `monthly_question_limit` 402; `paused`, `no_account` 403; `rate_limited` 429; `lab_error` 500.",
            "enum": ["bad_request", "invalid_key", "balance_empty", "monthly_spend_limit", "monthly_question_limit", "paused", "no_account", "rate_limited", "lab_error"]
          },
          "retry_after": {
            "type": "integer",
            "minimum": 1,
            "description": "Only with `rate_limited`: seconds to wait before the next request. The Retry-After header says the same."
          }
        },
        "additionalProperties": false
      },
      "StreamError": {
        "type": "object",
        "description": "The data of an `error` event: the answer stopped after the stream started. It costs nothing.",
        "required": ["message", "code"],
        "properties": {
          "message": {
            "type": "string",
            "description": "What happened, in plain words."
          },
          "code": {
            "const": "lab_error"
          }
        },
        "additionalProperties": false
      }
    },
    "headers": {
      "Retry-After": {
        "description": "Seconds to wait before the next request, the same as `retry_after` in the body.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`bad_request`: the body is empty, over 128,000 bytes or not one JSON object; the question is missing, not text, empty or over 12,000 characters; or `history` is not a list. The message says which.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "noQuestion": {
                "value": {
                  "error": "Send a question of 1 to 12,000 characters.",
                  "code": "bad_request"
                }
              }
            }
          }
        }
      },
      "InvalidKey": {
        "description": "`invalid_key`: the key is missing, wrong, revoked or replaced by Rotate.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalidKey": {
                "value": {
                  "error": "A valid API key is required: Authorization: Bearer klab_...",
                  "code": "invalid_key"
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "`balance_empty`: a prepaid balance is at $0 (a top-up starts questions again at once). `monthly_spend_limit` or `monthly_question_limit`: this month's limit is reached; limits start again on the 1st of the month, 00:00 UTC.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "balanceEmpty": {
                "value": {
                  "error": "This account has no balance left, so questions are paused. Top up at https://lab-dashboard.kaspa.news/dashboard#billing and they work again at once.",
                  "code": "balance_empty"
                }
              },
              "monthlyQuestions": {
                "value": {
                  "error": "This account reached its limit of 1,000 questions this month. It starts again on the 1st (UTC), or the lab owner can raise it.",
                  "code": "monthly_question_limit"
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "`paused`: the lab owner paused the account. `no_account`: the key belongs to no account.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "paused": {
                "value": {
                  "error": "This account is paused. Contact the lab owner.",
                  "code": "paused"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited`: more requests in the last minute than the account allows (30 unless the lab owner set another number). Wait `retry_after` seconds.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "rateLimited": {
                "value": {
                  "error": "Too many requests: this account allows 30 per minute. Try again in 12 seconds.",
                  "code": "rate_limited",
                  "retry_after": 12
                }
              }
            }
          }
        }
      },
      "LabError": {
        "description": "`lab_error`: something went wrong on the lab side, or the lab is starting up. Try again in a moment; it costs nothing.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "labError": {
                "value": {
                  "error": "Something went wrong on the lab side. Please try again in a moment.",
                  "code": "lab_error"
                }
              }
            }
          }
        }
      }
    }
  }
}
