{
  "name": "ninokroesen lab API",
  "version": 1,
  "localeNote": "Machine-readable documentation is English-only; the site itself is Dutch (.nl) and English (.com).",
  "baseUrl": "https://ninokroesen.com",
  "discovery": {
    "llmsTxt": "https://ninokroesen.com/llms.txt",
    "llmsFullTxt": "https://ninokroesen.com/llms-full.txt",
    "sitemap": "https://ninokroesen.com/sitemap.xml",
    "rss": "https://ninokroesen.com/rss.xml",
    "markdown": "Any public HTML page is also available as markdown: append .md to the path (the home page is /index.md), or send Accept: text/markdown on a URL with no .md suffix. The .md URLs are the cacheable ones."
  },
  "mcp": {
    "endpoint": "https://ninokroesen.com/mcp",
    "transport": "Streamable HTTP, JSON-RPC 2.0 over POST. No session id is issued; every request is independent.",
    "tools": [
      {
        "name": "render_text_as_png",
        "title": "Render text as PNG",
        "description": "Renders markdown or plain text into a single dense PNG (312-column grid, max 28080 input characters) using a bundled bitmap font, and returns the image plus token estimates for both representations. Use it when passing the text as an image would cost fewer input tokens than passing it as text — long logs, tables, dense reference material — and include the returned estimates when explaining the tradeoff.",
        "endpoint": "https://ninokroesen.com/mcp"
      },
      {
        "name": "unslopf_negative_keywords",
        "title": "unslopf negative keywords",
        "description": "Mechanical n-gram analysis (no LLM) over inline markdown, page URLs, or a sitemap. Returns the phrases that recur across the corpus but are not distinctive to it, with document frequencies and a ready-to-paste prompt block — a negative-keyword list for pushing a classifier away from filler. It compares documents against each other, so it needs at least two (a sitemap counts as one call but many documents); a single text returns nothing, and the tool says so rather than reporting success with an empty list.",
        "endpoint": "https://ninokroesen.com/mcp"
      }
    ],
    "methods": [
      "initialize",
      "notifications/initialized",
      "ping",
      "tools/list",
      "tools/call"
    ],
    "unsupported": "Only POST is served. GET and DELETE answer 405; resources, prompts, and sampling are not implemented."
  },
  "access": {
    "setting": "labs.agentAccess (admin: /admin/settings)",
    "disabledStatus": "503 with { ok: false, error: \"agent_access_disabled\" }",
    "ipBudgetPer10Min": "labs.agentRequestsPer10Min, shared across every endpoint below",
    "globalBudgetPer10Min": "labs.agentGlobalPer10Min",
    "rateLimitedStatus": "429 with Retry-After",
    "cors": "No CORS headers are sent; call from a server or a non-browser client."
  },
  "endpoints": [
    {
      "id": "textAsPng",
      "name": "Text as PNG",
      "page": "/render-text-as-png",
      "markdown": "/render-text-as-png.md",
      "mcpTool": "render_text_as_png",
      "summary": "Renders markdown or plain text into one dense PNG using a bundled 5x8 bitmap font. Built for vision models: the same text usually costs fewer input tokens as an image than as text, and the response reports both estimates.",
      "calls": [
        {
          "method": "POST",
          "path": "/api/render-text-as-png",
          "contentType": "text/markdown, text/plain, application/json ({text} or {markdown}), or form-encoded",
          "body": {
            "text": "string, required"
          },
          "returns": "image/png bytes, plus token estimates in the X-Est-* response headers",
          "maxChars": 28080
        },
        {
          "method": "GET",
          "path": "/api/render-text-as-png",
          "query": {
            "text": "string, required (URL-encoded)",
            "dl": "set to 1 for Content-Disposition: attachment"
          },
          "returns": "image/png bytes, same X-Est-* headers",
          "maxChars": 4000
        }
      ],
      "output": {
        "contentType": "image/png",
        "width": 1568,
        "maxHeight": 728,
        "headers": [
          "X-Input-Chars",
          "X-Input-Bytes",
          "X-Png-Bytes",
          "X-Png-Width",
          "X-Png-Height",
          "X-Est-Text-Tokens",
          "X-Est-Image-Tokens"
        ],
        "note": "Text is wrapped to a fixed 312-column grid and clipped to the max height; input newlines are kept as ↵ so line structure survives the image."
      }
    },
    {
      "id": "unslopf",
      "name": "unslopf",
      "page": "/unslopf",
      "markdown": "/unslopf.md",
      "mcpTool": "unslopf_negative_keywords",
      "summary": "Mechanical n-gram analysis (no LLM) over markdown you pass in, page URLs, or a sitemap. Returns the phrases that recur in that corpus but are not distinctive to it — a negative-keyword list to push a classifier away from filler — with document-frequency counts and a ready-to-paste prompt block. It compares documents against each other, so the corpus needs at least two: a single text has nothing to recur against and comes back empty.",
      "calls": [
        {
          "method": "POST",
          "path": "/api/unslopf",
          "aliases": [
            "/lab/unslopf"
          ],
          "contentType": "application/json, text/markdown, or text/plain",
          "body": {
            "text": "string or [{id, text, date}] — inline markdown",
            "urls": "array of up to 12 page URLs",
            "sitemap": "one sitemap URL, expanded into its pages",
            "keep": "array of tokens to always treat as distinctive",
            "recent": "how many documents count as recent (default 8, max 20)",
            "top": "how many phrases to return (default 25, max 40)"
          },
          "returns": "application/json — { ok, host, stats, documents, negative: { mixed, recent, global }, prompt }",
          "maxChars": 80000
        },
        {
          "method": "GET",
          "path": "/api/unslopf",
          "query": {
            "text": "inline markdown (URL-encoded)",
            "url": "one page URL",
            "sitemap": "one sitemap URL",
            "keep": "comma-separated tokens",
            "recent": "integer",
            "top": "integer"
          },
          "returns": "same JSON as the POST",
          "maxChars": 4000
        }
      ],
      "output": {
        "contentType": "application/json",
        "note": "Fetched pages are SSRF-guarded and the request body is never stored. Fetching a URL costs outbound time, so the GET variant accepts one URL per call.",
        "fetchTimeoutMs": 12000,
        "maxDocuments": 12,
        "maxTotalChars": 200000
      }
    },
    {
      "id": "appelEi",
      "name": "Appel & Ei Index",
      "page": "/appel-en-ei",
      "markdown": "/appel-en-ei.md",
      "mcpTool": null,
      "summary": "What one apple and one egg cost per Dutch supermarket chain, and the weighted national index built from them. Read-only: there is nothing to call, only data to read.",
      "calls": [
        {
          "method": "GET",
          "path": "/appel-en-ei.json",
          "returns": "application/json — the basket rows, the weighted index, and the recorded index series"
        }
      ],
      "output": {
        "contentType": "application/json",
        "note": "Values come from the supermarket basket, refreshed every few days. A failed fetch never overwrites a good value, so a row can carry stale: true and an older fetchedAt — report the age rather than the number alone. Anchor prices and the index move slowly by design."
      }
    }
  ],
  "errors": {
    "empty": "no input to work with",
    "invalid": "body was not a string, object, or array of the shape above",
    "too_long": "input exceeds maxChars (the response repeats the limit)",
    "too_many_urls": "more than 12 URLs in one call",
    "too_many_texts": "more than 12 documents in one call",
    "unsafe_url": "URL resolves to a private or reserved address (SSRF guard)",
    "fetch_failed": "the URL could not be fetched",
    "no_documents": "nothing usable came back from the sources",
    "too_tall": "rendered text exceeds the maximum image height",
    "busy": "both render/analyze slots are occupied; retry shortly",
    "too_few_documents": "the corpus had fewer than two documents — a phrase must recur across documents to qualify, so give it at least two pages",
    "rate_limited": "over the shared agent budget; see Retry-After",
    "agent_access_disabled": "the endpoints are switched off at /admin/settings"
  }
}