Skip to content

Add calcfi.app 1.0.0 (CalcFi Insights API)#2541

Open
jeresalmisto wants to merge 1 commit into
APIs-guru:mainfrom
jeresalmisto:add-calcfi-app
Open

Add calcfi.app 1.0.0 (CalcFi Insights API)#2541
jeresalmisto wants to merge 1 commit into
APIs-guru:mainfrom
jeresalmisto:add-calcfi-app

Conversation

@jeresalmisto
Copy link
Copy Markdown

Adds the CalcFi Insights API OpenAPI 3.1 spec.

Service: calcfi.app (free personal-finance calculator platform)
Version: 1.0.0
OpenAPI source: https://calcfi.app/api/insights/openapi.json (auto-served, always current)
Documentation: https://calcfi.app/developers
Sample: GET https://calcfi.app/api/insights returns aggregated decision-impact statistics from CalcFi's 300+ calculators.

The API is free, no API key required, served from a public Next.js endpoint on Vercel.

Companion ecosystem:

Maintained by Jere Salmisto (ORCID 0009-0000-0916-8684), founder of calcfi.app.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the OpenAPI 3.1.0 specification for the CalcFi Insights API, defining endpoints for aggregate data such as calculator runs and score distributions. Feedback suggests pretty-printing the minified JSON for better maintainability, adding missing k-anonymity constraints to the WatchAggregateRow schema, and including standard error responses (400, 500) to complete the API contract.

@@ -0,0 +1 @@
{"openapi":"3.1.0","info":{"title":"CalcFi Insights API","version":"1.0.0","description":"Public, read-only aggregate data feed from CalcFi.app. All rows are k-anonymity safe (count >= 10) and never include PII. Free for non-commercial use; attribution requested. Updated daily at 05:00 UTC.","contact":{"name":"CalcFi","url":"https://calcfi.app"},"license":{"name":"CC BY 4.0","url":"https://creativecommons.org/licenses/by/4.0/"}},"servers":[{"url":"https://calcfi.app","description":"Production"}],"tags":[{"name":"calc","description":"Calculator-run aggregates"},{"name":"distributions","description":"Score/burden distributions"},{"name":"sequences","description":"Calculator co-use patterns"},{"name":"watch","description":"Watch alert aggregates"},{"name":"meta","description":"API metadata"}],"paths":{"/api/insights/calc-aggregates":{"get":{"tags":["calc"],"summary":"Daily calculator-run counts banded by city/state/result","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"slug","in":"query","schema":{"type":"string","example":"mortgage-calculator"}},{"name":"city","in":"query","schema":{"type":"string","example":"austin"}},{"name":"state","in":"query","schema":{"type":"string","example":"TX"}},{"name":"band","in":"query","schema":{"type":"string","example":"1.5-2k"}}],"responses":{"200":{"$ref":"#/components/responses/CalcAggregates"}}}},"/api/insights/reality-score-dist":{"get":{"tags":["distributions"],"summary":"Reality-score distribution by city/age/income band","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"city","in":"query","schema":{"type":"string"}},{"name":"age_band","in":"query","schema":{"type":"string","example":"26-35"}},{"name":"income_band","in":"query","schema":{"type":"string","example":"50-75k"}},{"name":"score_band","in":"query","schema":{"type":"string","example":"40-60"}}],"responses":{"200":{"$ref":"#/components/responses/RealityScoreDist"}}}},"/api/insights/cost-burden-dist":{"get":{"tags":["distributions"],"summary":"Cost-burden distribution (housing/income ratio bands)","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"city","in":"query","schema":{"type":"string"}},{"name":"income_band","in":"query","schema":{"type":"string"}},{"name":"burden_band","in":"query","schema":{"type":"string","example":"30-40%"}}],"responses":{"200":{"$ref":"#/components/responses/CostBurdenDist"}}}},"/api/insights/sequence-patterns":{"get":{"tags":["sequences"],"summary":"Calculator co-use patterns (prev → next slug counts)","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"prev_slug","in":"query","schema":{"type":"string"}},{"name":"next_slug","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/SequencePatterns"}}}},"/api/insights/watch-aggregates":{"get":{"tags":["watch"],"summary":"Watch-alert pin/fire counts banded by source/threshold","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"source","in":"query","schema":{"type":"string","example":"mortgage-rate-30y"}},{"name":"operator","in":"query","schema":{"type":"string","example":"lt"}},{"name":"threshold_band","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/WatchAggregates"}}}},"/api/insights/health":{"get":{"tags":["meta"],"summary":"Liveness probe","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"ts":{"type":"string","format":"date-time"},"tables":{"type":"integer"}}}}}}}}}},"components":{"schemas":{"Page":{"type":"object","required":["topic","since","count","next_cursor","rows"],"properties":{"topic":{"type":"string"},"since":{"type":"string","format":"date"},"count":{"type":"integer"},"next_cursor":{"type":["integer","null"]},"rows":{"type":"array","items":{}}}},"CalcAggregateRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"calc_slug":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"result_band":{"type":"string","description":"Banded result bucket — never raw value"},"count_runs":{"type":"integer","minimum":10}}},"RealityScoreRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"city":{"type":"string"},"age_band":{"type":"string","enum":["18-25","26-35","36-45","46-55","56-65","65+"]},"income_band":{"type":"string","enum":["<25k","25-50k","50-75k","75-100k","100-150k","150-250k","250-500k","500k+"]},"score_band":{"type":"string","enum":["0-20","20-40","40-60","60-80","80-100"]},"count":{"type":"integer","minimum":10}}},"CostBurdenRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"city":{"type":"string"},"income_band":{"type":"string"},"burden_band":{"type":"string","enum":["<20%","20-30%","30-40%","40-50%","50%+"]},"count":{"type":"integer","minimum":10}}},"SequencePatternRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"prev_slug":{"type":"string"},"next_slug":{"type":"string"},"count":{"type":"integer","minimum":10}}},"WatchAggregateRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"source":{"type":"string"},"operator":{"type":"string"},"threshold_band":{"type":"string"},"count_pinned":{"type":"integer"},"count_fired":{"type":"integer"}}}},"responses":{"CalcAggregates":{"description":"Page of calc-run aggregates","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/CalcAggregateRow"}}}}]},"examples":{"mortgageInTexas":{"summary":"Mortgage payment runs in Texas, last 30 days","value":{"topic":"calc-aggregates","since":"2026-04-01","count":3,"next_cursor":null,"rows":[{"day":"2026-04-15","calc_slug":"mortgage-payment","city":"austin","state":"TX","result_band":"2-2.5k","count_runs":18},{"day":"2026-04-15","calc_slug":"mortgage-payment","city":"dallas","state":"TX","result_band":"1.5-2k","count_runs":22},{"day":"2026-04-16","calc_slug":"mortgage-payment","city":"houston","state":"TX","result_band":"1.5-2k","count_runs":14}]}},"paginated":{"summary":"Paginated response with cursor for next page","value":{"topic":"calc-aggregates","since":"2026-02-01","count":200,"next_cursor":200,"rows":[{"day":"2026-04-20","calc_slug":"rent-vs-buy","city":"seattle","state":"WA","result_band":"buy-favored","count_runs":31}]}}}}}},"RealityScoreDist":{"description":"Page of reality-score distribution rows","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/RealityScoreRow"}}}}]},"examples":{"millennialMidIncome":{"summary":"Reality score distribution for 26-35 / $50-75k cohort","value":{"topic":"reality-score-dist","since":"2026-04-01","count":4,"next_cursor":null,"rows":[{"day":"2026-04-15","city":"austin","age_band":"26-35","income_band":"50-75k","score_band":"40-60","count":24},{"day":"2026-04-15","city":"austin","age_band":"26-35","income_band":"50-75k","score_band":"60-80","count":17},{"day":"2026-04-15","city":"denver","age_band":"26-35","income_band":"50-75k","score_band":"40-60","count":12},{"day":"2026-04-16","city":"denver","age_band":"26-35","income_band":"50-75k","score_band":"20-40","count":11}]}}}}}},"CostBurdenDist":{"description":"Page of cost-burden distribution rows","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/CostBurdenRow"}}}}]},"examples":{"austinBurden":{"summary":"Cost-burden distribution for Austin renters/buyers","value":{"topic":"cost-burden-dist","since":"2026-04-01","count":3,"next_cursor":null,"rows":[{"day":"2026-04-15","city":"austin","income_band":"50-75k","burden_band":"30-40%","count":28},{"day":"2026-04-15","city":"austin","income_band":"50-75k","burden_band":"40-50%","count":19},{"day":"2026-04-15","city":"austin","income_band":"75-100k","burden_band":"20-30%","count":22}]}}}}}},"SequencePatterns":{"description":"Page of calculator co-use patterns","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/SequencePatternRow"}}}}]},"examples":{"mortgageThenAffordability":{"summary":"Users who run mortgage-payment commonly run affordability next","value":{"topic":"sequence-patterns","since":"2026-04-01","count":3,"next_cursor":null,"rows":[{"day":"2026-04-15","prev_slug":"mortgage-payment","next_slug":"home-affordability","count":47},{"day":"2026-04-15","prev_slug":"mortgage-payment","next_slug":"rent-vs-buy","count":31},{"day":"2026-04-16","prev_slug":"home-affordability","next_slug":"closing-costs","count":18}]}}}}}},"WatchAggregates":{"description":"Page of watch-alert aggregates","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/WatchAggregateRow"}}}}]},"examples":{"mortgageRateWatch":{"summary":"30-year mortgage rate watch pins and fires","value":{"topic":"watch-aggregates","since":"2026-04-01","count":2,"next_cursor":null,"rows":[{"day":"2026-04-15","source":"mortgage-rate-30y","operator":"lt","threshold_band":"6-6.5%","count_pinned":142,"count_fired":38},{"day":"2026-04-15","source":"mortgage-rate-30y","operator":"lt","threshold_band":"6.5-7%","count_pinned":89,"count_fired":71}]}}}}}}}}} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The OpenAPI specification is provided as a single minified line, which makes it difficult to review changes and maintain the file. It is standard practice to store such specifications in a pretty-printed format. Additionally, the WatchAggregateRow schema is missing the minimum: 10 constraint on its count fields (count_pinned and count_fired), which is inconsistent with the k-anonymity policy (count >= 10) described in the API info and enforced in other row schemas.

{
  "openapi": "3.1.0",
  "info": {
    "title": "CalcFi Insights API",
    "version": "1.0.0",
    "description": "Public, read-only aggregate data feed from CalcFi.app. All rows are k-anonymity safe (count >= 10) and never include PII. Free for non-commercial use; attribution requested. Updated daily at 05:00 UTC.",
    "contact": {
      "name": "CalcFi",
      "url": "https://calcfi.app"
    },
    "license": {
      "name": "CC BY 4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    }
  },
  "servers": [
    {
      "url": "https://calcfi.app",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "calc",
      "description": "Calculator-run aggregates"
    },
    {
      "name": "distributions",
      "description": "Score/burden distributions"
    },
    {
      "name": "sequences",
      "description": "Calculator co-use patterns"
    },
    {
      "name": "watch",
      "description": "Watch alert aggregates"
    },
    {
      "name": "meta",
      "description": "API metadata"
    }
  ],
  "paths": {
    "/api/insights/calc-aggregates": {
      "get": {
        "tags": [
          "calc"
        ],
        "summary": "Daily calculator-run counts banded by city/state/result",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-04-01"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max rows per page (1-1000). Default 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Row offset for pagination — pass `next_cursor` from previous response.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "slug",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "mortgage-calculator"
            }
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "austin"
            }
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "TX"
            }
          },
          {
            "name": "band",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "1.5-2k"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CalcAggregates"
          }
        }
      }
    },
    "/api/insights/reality-score-dist": {
      "get": {
        "tags": [
          "distributions"
        ],
        "summary": "Reality-score distribution by city/age/income band",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-04-01"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max rows per page (1-1000). Default 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Row offset for pagination — pass `next_cursor` from previous response.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "age_band",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "26-35"
            }
          },
          {
            "name": "income_band",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "50-75k"
            }
          },
          {
            "name": "score_band",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "40-60"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/RealityScoreDist"
          }
        }
      }
    },
    "/api/insights/cost-burden-dist": {
      "get": {
        "tags": [
          "distributions"
        ],
        "summary": "Cost-burden distribution (housing/income ratio bands)",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-04-01"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max rows per page (1-1000). Default 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Row offset for pagination — pass `next_cursor` from previous response.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "income_band",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "burden_band",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "30-40%"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CostBurdenDist"
          }
        }
      }
    },
    "/api/insights/sequence-patterns": {
      "get": {
        "tags": [
          "sequences"
        ],
        "summary": "Calculator co-use patterns (prev → next slug counts)",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-04-01"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max rows per page (1-1000). Default 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Row offset for pagination — pass `next_cursor` from previous response.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "prev_slug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "next_slug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/SequencePatterns"
          }
        }
      }
    },
    "/api/insights/watch-aggregates": {
      "get": {
        "tags": [
          "watch"
        ],
        "summary": "Watch-alert pin/fire counts banded by source/threshold",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-04-01"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max rows per page (1-1000). Default 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Row offset for pagination — pass `next_cursor` from previous response.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "mortgage-rate-30y"
            }
          },
          {
            "name": "operator",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "lt"
            }
          },
          {
            "name": "threshold_band",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/WatchAggregates"
          }
        }
      }
    },
    "/api/insights/health": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "Liveness probe",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "ts": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "tables": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Page": {
        "type": "object",
        "required": [
          "topic",
          "since",
          "count",
          "next_cursor",
          "rows"
        ],
        "properties": {
          "topic": {
            "type": "string"
          },
          "since": {
            "type": "string",
            "format": "date"
          },
          "count": {
            "type": "integer"
          },
          "next_cursor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rows": {
            "type": "array",
            "items": {}
          }
        }
      },
      "CalcAggregateRow": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "calc_slug": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "result_band": {
            "type": "string",
            "description": "Banded result bucket — never raw value"
          },
          "count_runs": {
            "type": "integer",
            "minimum": 10
          }
        }
      },
      "RealityScoreRow": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "city": {
            "type": "string"
          },
          "age_band": {
            "type": "string",
            "enum": [
              "18-25",
              "26-35",
              "36-45",
              "46-55",
              "56-65",
              "65+"
            ]
          },
          "income_band": {
            "type": "string",
            "enum": [
              "<25k",
              "25-50k",
              "50-75k",
              "75-100k",
              "100-150k",
              "150-250k",
              "250-500k",
              "500k+"
            ]
          },
          "score_band": {
            "type": "string",
            "enum": [
              "0-20",
              "20-40",
              "40-60",
              "60-80",
              "80-100"
            ]
          },
          "count": {
            "type": "integer",
            "minimum": 10
          }
        }
      },
      "CostBurdenRow": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "city": {
            "type": "string"
          },
          "income_band": {
            "type": "string"
          },
          "burden_band": {
            "type": "string",
            "enum": [
              "<20%",
              "20-30%",
              "30-40%",
              "40-50%",
              "50%+"
            ]
          },
          "count": {
            "type": "integer",
            "minimum": 10
          }
        }
      },
      "SequencePatternRow": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "prev_slug": {
            "type": "string"
          },
          "next_slug": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "minimum": 10
          }
        }
      },
      "WatchAggregateRow": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "source": {
            "type": "string"
          },
          "operator": {
            "type": "string"
          },
          "threshold_band": {
            "type": "string"
          },
          "count_pinned": {
            "type": "integer",
            "minimum": 10
          },
          "count_fired": {
            "type": "integer",
            "minimum": 10
          }
        }
      }
    },
    "responses": {
      "CalcAggregates": {
        "description": "Page of calc-run aggregates",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Page"
                },
                {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CalcAggregateRow"
                      }
                    }
                  }
                }
              ]
            },
            "examples": {
              "mortgageInTexas": {
                "summary": "Mortgage payment runs in Texas, last 30 days",
                "value": {
                  "topic": "calc-aggregates",
                  "since": "2026-04-01",
                  "count": 3,
                  "next_cursor": null,
                  "rows": [
                    {
                      "day": "2026-04-15",
                      "calc_slug": "mortgage-payment",
                      "city": "austin",
                      "state": "TX",
                      "result_band": "2-2.5k",
                      "count_runs": 18
                    },
                    {
                      "day": "2026-04-15",
                      "calc_slug": "mortgage-payment",
                      "city": "dallas",
                      "state": "TX",
                      "result_band": "1.5-2k",
                      "count_runs": 22
                    },
                    {
                      "day": "2026-04-16",
                      "calc_slug": "mortgage-payment",
                      "city": "houston",
                      "state": "TX",
                      "result_band": "1.5-2k",
                      "count_runs": 14
                    }
                  ]
                }
              },
              "paginated": {
                "summary": "Paginated response with cursor for next page",
                "value": {
                  "topic": "calc-aggregates",
                  "since": "2026-02-01",
                  "count": 200,
                  "next_cursor": 200,
                  "rows": [
                    {
                      "day": "2026-04-20",
                      "calc_slug": "rent-vs-buy",
                      "city": "seattle",
                      "state": "WA",
                      "result_band": "buy-favored",
                      "count_runs": 31
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "RealityScoreDist": {
        "description": "Page of reality-score distribution rows",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Page"
                },
                {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RealityScoreRow"
                      }
                    }
                  }
                }
              ]
            },
            "examples": {
              "millennialMidIncome": {
                "summary": "Reality score distribution for 26-35 / $50-75k cohort",
                "value": {
                  "topic": "reality-score-dist",
                  "since": "2026-04-01",
                  "count": 4,
                  "next_cursor": null,
                  "rows": [
                    {
                      "day": "2026-04-15",
                      "city": "austin",
                      "age_band": "26-35",
                      "income_band": "50-75k",
                      "score_band": "40-60",
                      "count": 24
                    },
                    {
                      "day": "2026-04-15",
                      "city": "austin",
                      "age_band": "26-35",
                      "income_band": "50-75k",
                      "score_band": "60-80",
                      "count": 17
                    },
                    {
                      "day": "2026-04-15",
                      "city": "denver",
                      "age_band": "26-35",
                      "income_band": "50-75k",
                      "score_band": "40-60",
                      "count": 12
                    },
                    {
                      "day": "2026-04-16",
                      "city": "denver",
                      "age_band": "26-35",
                      "income_band": "50-75k",
                      "score_band": "20-40",
                      "count": 11
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "CostBurdenDist": {
        "description": "Page of cost-burden distribution rows",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Page"
                },
                {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CostBurdenRow"
                      }
                    }
                  }
                }
              ]
            },
            "examples": {
              "austinBurden": {
                "summary": "Cost-burden distribution for Austin renters/buyers",
                "value": {
                  "topic": "cost-burden-dist",
                  "since": "2026-04-01",
                  "count": 3,
                  "next_cursor": null,
                  "rows": [
                    {
                      "day": "2026-04-15",
                      "city": "austin",
                      "income_band": "50-75k",
                      "burden_band": "30-40%",
                      "count": 28
                    },
                    {
                      "day": "2026-04-15",
                      "city": "austin",
                      "income_band": "50-75k",
                      "burden_band": "40-50%",
                      "count": 19
                    },
                    {
                      "day": "2026-04-15",
                      "city": "austin",
                      "income_band": "75-100k",
                      "burden_band": "20-30%",
                      "count": 22
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "SequencePatterns": {
        "description": "Page of calculator co-use patterns",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Page"
                },
                {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SequencePatternRow"
                      }
                    }
                  }
                }
              ]
            },
            "examples": {
              "mortgageThenAffordability": {
                "summary": "Users who run mortgage-payment commonly run affordability next",
                "value": {
                  "topic": "sequence-patterns",
                  "since": "2026-04-01",
                  "count": 3,
                  "next_cursor": null,
                  "rows": [
                    {
                      "day": "2026-04-15",
                      "prev_slug": "mortgage-payment",
                      "next_slug": "home-affordability",
                      "count": 47
                    },
                    {
                      "day": "2026-04-15",
                      "prev_slug": "mortgage-payment",
                      "next_slug": "rent-vs-buy",
                      "count": 31
                    },
                    {
                      "day": "2026-04-16",
                      "prev_slug": "home-affordability",
                      "next_slug": "closing-costs",
                      "count": 18
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "WatchAggregates": {
        "description": "Page of watch-alert aggregates",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Page"
                },
                {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatchAggregateRow"
                      }
                    }
                  }
                }
              ]
            },
            "examples": {
              "mortgageRateWatch": {
                "summary": "30-year mortgage rate watch pins and fires",
                "value": {
                  "topic": "watch-aggregates",
                  "since": "2026-04-01",
                  "count": 2,
                  "next_cursor": null,
                  "rows": [
                    {
                      "day": "2026-04-15",
                      "source": "mortgage-rate-30y",
                      "operator": "lt",
                      "threshold_band": "6-6.5%",
                      "count_pinned": 142,
                      "count_fired": 38
                    },
                    {
                      "day": "2026-04-15",
                      "source": "mortgage-rate-30y",
                      "operator": "lt",
                      "threshold_band": "6.5-7%",
                      "count_pinned": 89,
                      "count_fired": 71
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}

@@ -0,0 +1 @@
{"openapi":"3.1.0","info":{"title":"CalcFi Insights API","version":"1.0.0","description":"Public, read-only aggregate data feed from CalcFi.app. All rows are k-anonymity safe (count >= 10) and never include PII. Free for non-commercial use; attribution requested. Updated daily at 05:00 UTC.","contact":{"name":"CalcFi","url":"https://calcfi.app"},"license":{"name":"CC BY 4.0","url":"https://creativecommons.org/licenses/by/4.0/"}},"servers":[{"url":"https://calcfi.app","description":"Production"}],"tags":[{"name":"calc","description":"Calculator-run aggregates"},{"name":"distributions","description":"Score/burden distributions"},{"name":"sequences","description":"Calculator co-use patterns"},{"name":"watch","description":"Watch alert aggregates"},{"name":"meta","description":"API metadata"}],"paths":{"/api/insights/calc-aggregates":{"get":{"tags":["calc"],"summary":"Daily calculator-run counts banded by city/state/result","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"slug","in":"query","schema":{"type":"string","example":"mortgage-calculator"}},{"name":"city","in":"query","schema":{"type":"string","example":"austin"}},{"name":"state","in":"query","schema":{"type":"string","example":"TX"}},{"name":"band","in":"query","schema":{"type":"string","example":"1.5-2k"}}],"responses":{"200":{"$ref":"#/components/responses/CalcAggregates"}}}},"/api/insights/reality-score-dist":{"get":{"tags":["distributions"],"summary":"Reality-score distribution by city/age/income band","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"city","in":"query","schema":{"type":"string"}},{"name":"age_band","in":"query","schema":{"type":"string","example":"26-35"}},{"name":"income_band","in":"query","schema":{"type":"string","example":"50-75k"}},{"name":"score_band","in":"query","schema":{"type":"string","example":"40-60"}}],"responses":{"200":{"$ref":"#/components/responses/RealityScoreDist"}}}},"/api/insights/cost-burden-dist":{"get":{"tags":["distributions"],"summary":"Cost-burden distribution (housing/income ratio bands)","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"city","in":"query","schema":{"type":"string"}},{"name":"income_band","in":"query","schema":{"type":"string"}},{"name":"burden_band","in":"query","schema":{"type":"string","example":"30-40%"}}],"responses":{"200":{"$ref":"#/components/responses/CostBurdenDist"}}}},"/api/insights/sequence-patterns":{"get":{"tags":["sequences"],"summary":"Calculator co-use patterns (prev → next slug counts)","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"prev_slug","in":"query","schema":{"type":"string"}},{"name":"next_slug","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/SequencePatterns"}}}},"/api/insights/watch-aggregates":{"get":{"tags":["watch"],"summary":"Watch-alert pin/fire counts banded by source/threshold","parameters":[{"name":"since","in":"query","description":"Earliest day to include (YYYY-MM-DD, UTC). Defaults to 90 days ago.","schema":{"type":"string","format":"date","example":"2026-04-01"}},{"name":"limit","in":"query","description":"Max rows per page (1-1000). Default 200.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":200}},{"name":"cursor","in":"query","description":"Row offset for pagination — pass `next_cursor` from previous response.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"source","in":"query","schema":{"type":"string","example":"mortgage-rate-30y"}},{"name":"operator","in":"query","schema":{"type":"string","example":"lt"}},{"name":"threshold_band","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/WatchAggregates"}}}},"/api/insights/health":{"get":{"tags":["meta"],"summary":"Liveness probe","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"ts":{"type":"string","format":"date-time"},"tables":{"type":"integer"}}}}}}}}}},"components":{"schemas":{"Page":{"type":"object","required":["topic","since","count","next_cursor","rows"],"properties":{"topic":{"type":"string"},"since":{"type":"string","format":"date"},"count":{"type":"integer"},"next_cursor":{"type":["integer","null"]},"rows":{"type":"array","items":{}}}},"CalcAggregateRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"calc_slug":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"result_band":{"type":"string","description":"Banded result bucket — never raw value"},"count_runs":{"type":"integer","minimum":10}}},"RealityScoreRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"city":{"type":"string"},"age_band":{"type":"string","enum":["18-25","26-35","36-45","46-55","56-65","65+"]},"income_band":{"type":"string","enum":["<25k","25-50k","50-75k","75-100k","100-150k","150-250k","250-500k","500k+"]},"score_band":{"type":"string","enum":["0-20","20-40","40-60","60-80","80-100"]},"count":{"type":"integer","minimum":10}}},"CostBurdenRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"city":{"type":"string"},"income_band":{"type":"string"},"burden_band":{"type":"string","enum":["<20%","20-30%","30-40%","40-50%","50%+"]},"count":{"type":"integer","minimum":10}}},"SequencePatternRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"prev_slug":{"type":"string"},"next_slug":{"type":"string"},"count":{"type":"integer","minimum":10}}},"WatchAggregateRow":{"type":"object","properties":{"day":{"type":"string","format":"date"},"source":{"type":"string"},"operator":{"type":"string"},"threshold_band":{"type":"string"},"count_pinned":{"type":"integer"},"count_fired":{"type":"integer"}}}},"responses":{"CalcAggregates":{"description":"Page of calc-run aggregates","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/CalcAggregateRow"}}}}]},"examples":{"mortgageInTexas":{"summary":"Mortgage payment runs in Texas, last 30 days","value":{"topic":"calc-aggregates","since":"2026-04-01","count":3,"next_cursor":null,"rows":[{"day":"2026-04-15","calc_slug":"mortgage-payment","city":"austin","state":"TX","result_band":"2-2.5k","count_runs":18},{"day":"2026-04-15","calc_slug":"mortgage-payment","city":"dallas","state":"TX","result_band":"1.5-2k","count_runs":22},{"day":"2026-04-16","calc_slug":"mortgage-payment","city":"houston","state":"TX","result_band":"1.5-2k","count_runs":14}]}},"paginated":{"summary":"Paginated response with cursor for next page","value":{"topic":"calc-aggregates","since":"2026-02-01","count":200,"next_cursor":200,"rows":[{"day":"2026-04-20","calc_slug":"rent-vs-buy","city":"seattle","state":"WA","result_band":"buy-favored","count_runs":31}]}}}}}},"RealityScoreDist":{"description":"Page of reality-score distribution rows","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/RealityScoreRow"}}}}]},"examples":{"millennialMidIncome":{"summary":"Reality score distribution for 26-35 / $50-75k cohort","value":{"topic":"reality-score-dist","since":"2026-04-01","count":4,"next_cursor":null,"rows":[{"day":"2026-04-15","city":"austin","age_band":"26-35","income_band":"50-75k","score_band":"40-60","count":24},{"day":"2026-04-15","city":"austin","age_band":"26-35","income_band":"50-75k","score_band":"60-80","count":17},{"day":"2026-04-15","city":"denver","age_band":"26-35","income_band":"50-75k","score_band":"40-60","count":12},{"day":"2026-04-16","city":"denver","age_band":"26-35","income_band":"50-75k","score_band":"20-40","count":11}]}}}}}},"CostBurdenDist":{"description":"Page of cost-burden distribution rows","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/CostBurdenRow"}}}}]},"examples":{"austinBurden":{"summary":"Cost-burden distribution for Austin renters/buyers","value":{"topic":"cost-burden-dist","since":"2026-04-01","count":3,"next_cursor":null,"rows":[{"day":"2026-04-15","city":"austin","income_band":"50-75k","burden_band":"30-40%","count":28},{"day":"2026-04-15","city":"austin","income_band":"50-75k","burden_band":"40-50%","count":19},{"day":"2026-04-15","city":"austin","income_band":"75-100k","burden_band":"20-30%","count":22}]}}}}}},"SequencePatterns":{"description":"Page of calculator co-use patterns","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/SequencePatternRow"}}}}]},"examples":{"mortgageThenAffordability":{"summary":"Users who run mortgage-payment commonly run affordability next","value":{"topic":"sequence-patterns","since":"2026-04-01","count":3,"next_cursor":null,"rows":[{"day":"2026-04-15","prev_slug":"mortgage-payment","next_slug":"home-affordability","count":47},{"day":"2026-04-15","prev_slug":"mortgage-payment","next_slug":"rent-vs-buy","count":31},{"day":"2026-04-16","prev_slug":"home-affordability","next_slug":"closing-costs","count":18}]}}}}}},"WatchAggregates":{"description":"Page of watch-alert aggregates","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/WatchAggregateRow"}}}}]},"examples":{"mortgageRateWatch":{"summary":"30-year mortgage rate watch pins and fires","value":{"topic":"watch-aggregates","since":"2026-04-01","count":2,"next_cursor":null,"rows":[{"day":"2026-04-15","source":"mortgage-rate-30y","operator":"lt","threshold_band":"6-6.5%","count_pinned":142,"count_fired":38},{"day":"2026-04-15","source":"mortgage-rate-30y","operator":"lt","threshold_band":"6.5-7%","count_pinned":89,"count_fired":71}]}}}}}}}}} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The API paths only define a 200 OK response. It is recommended to define standard error responses (e.g., 400 Bad Request for invalid query parameters like since or limit, and 500 Internal Server Error) to provide a complete contract for API consumers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant