{
  "openapi": "3.1.0",
  "info": {
    "title": "Fron Research Agent API",
    "version": "1.1.0",
    "summary": "Public reference data and free sample days for AI agents, from Fron Research and its Check sites.",
    "description": "One route, `/api/agent`, served by fronresearch.com (every site) and by each product site's own domain (that site's pages and sample day by default; `site=all` searches every site). No key, no account, no model call, no state between requests. Requests are logged without IP addresses so we can improve answers ([privacy](https://fronresearch.com/privacy/)); send only public questions.\n\n- `GET /api/agent` describes what is covered: the five Check sites (HeatCheck, Bowling Data, Trampoline Park Data, Golf Sim Data, CourtCheck), their free example days and every document id.\n- `GET /api/agent?q=` answers a plain-language question with the most relevant passages from every public page, each with the canonical URL to cite.\n- `GET /api/agent?doc=` reads one page; `doc=sample/<site>` returns that site's free example day as rows (CC BY 4.0).\n- `POST /api/agent` takes `{\"query\": ...}` (or `{\"id\": ...}`) as JSON, or JSON-RPC 2.0 for MCP (streamable HTTP, stateless) with the tools `about`, `search` and `fetch`.\n\nThe corpus is rebuilt from the sites' current public pages on every fronresearch.com deploy. Figures are modeled: revenue is booking value (GBV) with a P10-P90 range, and unknown is never zero.",
    "contact": {
      "name": "Fron Research",
      "email": "luke@fronresearch.com",
      "url": "https://fronresearch.com/"
    }
  },
  "servers": [
    {
      "url": "https://fronresearch.com",
      "description": "Production hub: every site"
    },
    {
      "url": "https://heatcheck.tech",
      "description": "HeatCheck door: scoped to HeatCheck (saunas and bathhouses)"
    },
    {
      "url": "https://bowlingdata.co",
      "description": "Bowling Data door: scoped to Bowling Data (bowling centers)"
    },
    {
      "url": "https://golfsimdata.com",
      "description": "Golf Sim Data door: scoped to Golf Sim Data (indoor golf simulator studios)"
    },
    {
      "url": "https://trampolineparkdata.com",
      "description": "Trampoline Park Data door: scoped to Trampoline Park Data (trampoline and adventure parks)"
    },
    {
      "url": "https://preview.courtcheck.pages.dev",
      "description": "CourtCheck door: scoped to CourtCheck (pickleball and padel clubs)"
    }
  ],
  "paths": {
    "/api/agent": {
      "get": {
        "operationId": "agent",
        "summary": "Describe, search or read",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500
            },
            "description": "A question in plain language. Present → search."
          },
          {
            "name": "intent",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 300
            },
            "description": "Optional: why you are asking and for whom, in a sentence (e.g. \"sizing the NYC sauna market for an investor memo\"). Does not change the results; it tells Fron Research which data to publish next. No personal information."
          },
          {
            "name": "doc",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9/_-]{0,120}$",
              "examples": [
                "heatcheck/pricing",
                "sample/heatcheck"
              ]
            },
            "description": "A document id. Present → read that document (takes precedence over q)."
          },
          {
            "name": "site",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/SiteId"
            },
            "description": "Limit search to one site, or `all` for every site. Default: every site at fronresearch.com, the door's own site on a product site's domain."
          },
          {
            "name": "max",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 5
            },
            "description": "Results to return."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Continue a long document from `nextOffset`."
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "md",
                "json"
              ],
              "default": "md"
            },
            "description": "Markdown (default) or JSON. `Accept: application/json` also selects JSON."
          }
        ],
        "responses": {
          "200": {
            "description": "With no parameters: Info. With `q`: SearchResult. With `doc`: Document or SampleDay.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Info"
                    },
                    {
                      "$ref": "#/components/schemas/SearchResult"
                    },
                    {
                      "$ref": "#/components/schemas/Document"
                    },
                    {
                      "$ref": "#/components/schemas/SampleDay"
                    }
                  ]
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameter or body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown document id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`Accept: text/event-stream`: this MCP server is stateless and offers no stream"
          }
        }
      },
      "post": {
        "operationId": "agentPost",
        "summary": "JSON search, read, contact note, or MCP JSON-RPC",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SearchRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "id"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "pattern": "^[a-z0-9][a-z0-9/_-]{0,120}$",
                        "examples": [
                          "heatcheck/pricing",
                          "sample/heatcheck"
                        ]
                      },
                      "offset": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "mode",
                      "message"
                    ],
                    "properties": {
                      "mode": {
                        "const": "contact"
                      },
                      "message": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 2000,
                        "description": "What you are researching, or anything else for Fron Research. A person reads every note."
                      },
                      "email": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Optional reply address."
                      }
                    }
                  },
                  {
                    "$ref": "#/components/schemas/JsonRpcRequest"
                  },
                  {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 20,
                    "items": {
                      "$ref": "#/components/schemas/JsonRpcRequest"
                    }
                  }
                ]
              },
              "examples": {
                "search": {
                  "value": {
                    "query": "how does Bowling Data measure lane sell-through",
                    "site": "lanecheck",
                    "maxResults": 3
                  }
                },
                "mcp": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "tools/call",
                    "params": {
                      "name": "search",
                      "arguments": {
                        "query": "sauna revenue range"
                      }
                    }
                  }
                },
                "contact": {
                  "value": {
                    "mode": "contact",
                    "message": "Sizing the NYC sauna market for an investor memo; interested in monthly venue revenue.",
                    "email": "analyst@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SearchResult, Document, SampleDay, or a JSON-RPC response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "202": {
            "description": "Contact note received, or JSON-RPC notification accepted"
          },
          "400": {
            "description": "Invalid parameter or body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body over 16 KB"
          },
          "415": {
            "description": "Body is not application/json"
          }
        }
      },
      "options": {
        "operationId": "agentPreflight",
        "summary": "CORS preflight",
        "responses": {
          "204": {
            "description": "Any origin may call this API"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SiteId": {
        "type": "string",
        "enum": [
          "all",
          "heatcheck",
          "lanecheck",
          "parkcheck",
          "baycheck",
          "courtcheck",
          "fronresearch"
        ]
      },
      "SearchRequest": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "intent": {
            "type": "string",
            "maxLength": 300,
            "description": "Optional: why you are asking and for whom, in a sentence (e.g. \"sizing the NYC sauna market for an investor memo\"). Does not change the results; it tells Fron Research which data to publish next. No personal information."
          },
          "site": {
            "$ref": "#/components/schemas/SiteId"
          },
          "maxResults": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 5
          }
        }
      },
      "Passage": {
        "type": "object",
        "required": [
          "id",
          "title",
          "site",
          "heading",
          "url",
          "passage"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Document id; read it in full with `doc=`."
          },
          "title": {
            "type": "string"
          },
          "site": {
            "$ref": "#/components/schemas/SiteId"
          },
          "siteName": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "reference",
              "sample"
            ]
          },
          "heading": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical URL to cite."
          },
          "markdownUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "passage": {
            "type": "string",
            "description": "Markdown text of the passage."
          },
          "score": {
            "type": "number"
          }
        }
      },
      "SampleSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "site": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "market": {
            "type": "string"
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "venues": {
            "type": "integer"
          },
          "license": {
            "type": "string"
          },
          "attribution": {
            "type": "string"
          },
          "fetch": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "required": [
          "receivedQuery",
          "evidence",
          "answer",
          "results",
          "samples",
          "followUp"
        ],
        "properties": {
          "receivedQuery": {
            "type": "string"
          },
          "site": {
            "type": [
              "string",
              "null"
            ]
          },
          "evidence": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "found",
                  "partial",
                  "none"
                ]
              },
              "terms": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "answer": {
            "type": "object",
            "required": [
              "kind",
              "text",
              "instruction"
            ],
            "description": "How to use the passages (since 1.1.0; 1.0 returned answer.text as a plain string). Not a written answer: no model runs inside this API.",
            "properties": {
              "kind": {
                "const": "public_source_evidence"
              },
              "text": {
                "type": "string",
                "description": "One sentence on what was found and what to do next."
              },
              "instruction": {
                "type": "string",
                "description": "The citation and figure rules every answer must keep."
              }
            }
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Passage"
            }
          },
          "samples": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SampleSummary"
            },
            "description": "Free example days of the sites the question names (all when it names none)."
          },
          "followUp": {
            "type": "object",
            "properties": {
              "required": {
                "const": false
              },
              "fetch": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri"
              },
              "questions": {
                "type": "array",
                "description": "Optional questions for the agent's user, present while the search carried no intent. Put the answers, in one sentence, in intent on the next search.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "question"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "question": {
                      "type": "string"
                    }
                  }
                }
              },
              "instruction": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "route": {
            "type": [
              "object",
              "null"
            ],
            "description": "At the hub only: set when a question names no site and fits several products about equally (for example \"pricing\"). The results are then each site's best page; ask again at the chosen site's door.",
            "properties": {
              "question": {
                "type": "string"
              },
              "options": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "$ref": "#/components/schemas/SiteId"
                    },
                    "name": {
                      "type": "string"
                    },
                    "covers": {
                      "type": "string"
                    },
                    "agentApi": {
                      "type": "string",
                      "format": "uri"
                    },
                    "ask": {
                      "type": "string",
                      "format": "uri"
                    },
                    "page": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Document": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "title",
          "url",
          "text",
          "offset",
          "nextOffset"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "reference"
            ]
          },
          "title": {
            "type": "string"
          },
          "site": {
            "type": "string"
          },
          "siteName": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdownUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated": {
            "type": [
              "string",
              "null"
            ]
          },
          "offset": {
            "type": "integer"
          },
          "nextOffset": {
            "type": [
              "integer",
              "null"
            ]
          },
          "length": {
            "type": "integer"
          },
          "text": {
            "type": "string",
            "description": "Up to 20,000 characters of Markdown."
          }
        }
      },
      "SampleDay": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "site",
          "market",
          "columns",
          "rows",
          "license",
          "attribution"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "const": "sample"
          },
          "site": {
            "type": "string"
          },
          "siteName": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "market": {
            "type": "string"
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "weekday": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary": {
            "type": [
              "string",
              "null"
            ],
            "description": "The market total for the day."
          },
          "caption": {
            "type": [
              "string",
              "null"
            ]
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "description": "One row per venue, ranked by modeled revenue, values as the site prints them. null = not measured, never zero."
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "license": {
            "const": "CC BY 4.0"
          },
          "attribution": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Info": {
        "type": "object",
        "description": "What the API covers: sites, sample days, document ids, the History API, freshness and rules.",
        "properties": {
          "api": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "about": {
            "type": "string"
          },
          "usage": {
            "type": "object"
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "samples": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "documents": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "kind": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "history": {
            "type": "object"
          },
          "freshness": {
            "type": "object",
            "properties": {
              "builtAt": {
                "type": "string",
                "format": "date-time"
              },
              "sourceCommit": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "rebuilt": {
                "type": "string"
              }
            }
          },
          "rules": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "contact": {
            "type": "string"
          }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": [
          "jsonrpc",
          "method"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer"
            ]
          },
          "method": {
            "type": "string",
            "enum": [
              "initialize",
              "notifications/initialized",
              "ping",
              "tools/list",
              "tools/call"
            ]
          },
          "params": {
            "type": "object"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "help": {
            "type": "string"
          }
        }
      }
    }
  }
}
