> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Analytics

> Six endpoints for activity heatmaps, hotspots, commuter flows, spatial diagnostics, and model backtesting against historical Locus cell scores.

## `GET` `/api/activity-heatmap`

National activity feed aggregated to H3 resolution-5 hexagons. Returns a GeoJSON `FeatureCollection` of geocoded events from the last 90 days, weighted by event count per cell. Use this to power map-based heatmap visualizations of where activity is happening across the country, without scoping to a single metro.

The response is cached at the edge for 1 hour (`s-maxage=3600`, `stale-while-revalidate=86400`), so it's safe to fetch once on map mount instead of refetching as the viewport changes. There are no query parameters — the window and resolution are fixed.

Event counts per cell are long-tailed (p50 ≈ 22, p99 ≈ 270, max in the thousands). The `weight` field is the raw count; apply your own log or interpolation curve when styling heatmap layers so dense metros don't saturate the legend.

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/activity-heatmap"
```

**Mapbox GL example**

```js theme={null}
const res = await fetch("https://axiomlocus.io/api/activity-heatmap");
const fc = await res.json();

map.addSource("activity", { type: "geojson", data: fc });
map.addLayer({
  id: "activity-heatmap",
  type: "heatmap",
  source: "activity",
  paint: {
    "heatmap-weight": [
      "interpolate", ["linear"], ["get", "weight"],
      0, 0, 25, 0.2, 75, 0.45, 200, 0.75, 500, 1,
    ],
  },
});
```

**Response**

```json theme={null}
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [-122.4194, 37.7749] },
      "properties": { "weight": 287, "h3": "852830fffffffff" }
    }
  ],
  "meta": {
    "window_days": 90,
    "parent_res": 5,
    "hex_count": 412
  }
}
```

| Field                             | Type         | Description                                                 |
| --------------------------------- | ------------ | ----------------------------------------------------------- |
| `features[].geometry.coordinates` | `[lng, lat]` | Center of the H3 res-5 hexagon.                             |
| `features[].properties.weight`    | int          | Number of events that occurred in this hex over the window. |
| `features[].properties.h3`        | string       | H3 res-5 cell index.                                        |
| `meta.window_days`                | int          | Lookback window in days (currently `90`).                   |
| `meta.parent_res`                 | int          | H3 resolution of the aggregation (currently `5`).           |
| `meta.hex_count`                  | int          | Number of hexes with at least one event.                    |

## `GET` `/api/heatmap/[dimension]`

Generic single-dimension density heatmap. Returns a GeoJSON `FeatureCollection` of H3 res-5 hexagons weighted by the raw magnitude of the requested dimension, aggregated across the entire dataset (no metro scope, no time window). Use this to underlay a single raw signal beneath the Explorer's scored [`/api/activity-heatmap`](#get-apiactivity-heatmap) or [Fit](/locus/explorer#heatmap-layers) layers — for example, painting crime density under a Safety-weighted Fit map to spot cells that scored well *despite* elevated raw crime.

The route is public and rate-limited (surfaces `X-RateLimit-Remaining`), and its response is edge-cached for 1 hour (`s-maxage=3600`, `stale-while-revalidate=86400`).

**Path parameters**

| Name        | Type   | Description                                                                                                |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------- |
| `dimension` | string | Which dimension to aggregate. Currently supported: `crime`. Requesting an unknown dimension returns `400`. |

<Note>
  Each dimension maps to a source table and an optional magnitude column. When the dimension has no magnitude column, the weight is the count of rows per parent hex. Additional dimensions (for example, an H3-keyed rent-growth series) can be added as their source tables land; the response shape is identical.
</Note>

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/heatmap/crime"
```

**Response**

```json theme={null}
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [-122.4194, 37.7749] },
      "properties": { "h3": "852830fffffffff", "weight": 142 }
    }
  ],
  "meta": {
    "dimension": "crime",
    "parent_res": 5,
    "hex_count": 318
  }
}
```

| Field                             | Type         | Description                                                                                           |
| --------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
| `features[].geometry.coordinates` | `[lng, lat]` | Center of the H3 res-5 hexagon.                                                                       |
| `features[].properties.h3`        | string       | H3 res-5 cell index.                                                                                  |
| `features[].properties.weight`    | number       | Sum of the dimension's magnitude for this hex (row count when the dimension has no magnitude column). |
| `meta.dimension`                  | string       | Echoes the requested dimension.                                                                       |
| `meta.parent_res`                 | int          | H3 resolution of the aggregation (currently `5`).                                                     |
| `meta.hex_count`                  | int          | Number of hexes with a non-zero weight.                                                               |

**Errors**

| Status | When                                      |
| ------ | ----------------------------------------- |
| `400`  | The requested dimension is not supported. |
| `429`  | Rate limit exceeded.                      |
| `500`  | Backend query failed.                     |

## `GET` `/api/permits/hotspots`

H3-aggregated building permit hotspots for a metro, ranked by permit density and total estimated cost.

**Query parameters**

| Name         | Type   | Required | Default | Description                         |
| ------------ | ------ | -------- | ------- | ----------------------------------- |
| `metro_slug` | string | ✓        |         | Metro slug (e.g. sf, nyc, chi, la). |
| `days`       | int    |          | `90`    | Lookback period in days.            |
| `limit`      | int    |          | `25`    | Max hotspot cells to return.        |

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/permits/hotspots?metro_slug=sf&days=90&limit=10"
```

**Response**

```json theme={null}
{
  "metro": "sf",
  "days": 90,
  "hotspots": [
    {
      "h3_index": "882a100d63fffff",
      "center_lat": 37.7749,
      "center_lng": -122.4194,
      "permit_count": 47,
      "total_value": 12500000,
      "avg_value": 265957,
      "top_type": "new_construction"
    },
    {
      "h3_index": "882a100d65fffff",
      "center_lat": 37.7851,
      "center_lng": -122.4094,
      "permit_count": 32,
      "total_value": 8700000,
      "avg_value": 271875,
      "top_type": "renovation"
    }
  ]
}
```

| Field                     | Type   | Description                           |
| ------------------------- | ------ | ------------------------------------- |
| `hotspots`                | array  | H3 cells ranked by permit activity.   |
| `hotspots[].permit_count` | int    | Total permits in the lookback window. |
| `hotspots[].total_value`  | float  | Sum of estimated costs.               |
| `hotspots[].top_type`     | string | Most common permit type in this cell. |

## `GET` `/api/crime/hotspots`

Crime incident hotspots aggregated by H3 cell for a metro area.

**Query parameters**

| Name         | Type   | Required | Default | Description                                         |
| ------------ | ------ | -------- | ------- | --------------------------------------------------- |
| `metro_slug` | string | ✓        |         | Metro slug.                                         |
| `days`       | int    |          | `90`    | Lookback period in days.                            |
| `category`   | string |          |         | Filter by crime category: property, violent, other. |
| `limit`      | int    |          | `25`    | Max cells to return.                                |

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/crime/hotspots?metro_slug=sf&days=30&category=property"
```

**Response**

```json theme={null}
{
  "metro": "sf",
  "days": 30,
  "total_incidents": 1243,
  "hotspots": [
    {
      "h3_index": "882a100d63fffff",
      "incident_count": 89,
      "top_category": "property",
      "per_capita_rate": 12.4,
      "trend": "declining"
    }
  ]
}
```

| Field                        | Type   | Description                                              |
| ---------------------------- | ------ | -------------------------------------------------------- |
| `total_incidents`            | int    | Total incidents across all cells in the lookback window. |
| `hotspots[].incident_count`  | int    | Number of incidents in this H3 cell.                     |
| `hotspots[].per_capita_rate` | float  | Incidents per 1,000 residents.                           |
| `hotspots[].trend`           | string | Trend vs prior period: increasing, stable, declining.    |

## `GET` `/api/commuter-flows`

LEHD Origin-Destination commuter flow data for a Census tract. Shows where workers live and where residents work.

**Query parameters**

| Name        | Type   | Required | Default | Description                                                         |
| ----------- | ------ | -------- | ------- | ------------------------------------------------------------------- |
| `tract`     | string | ✓        |         | Census tract GEOID (11 digits).                                     |
| `direction` | string |          | `both`  | inbound (workers coming in), outbound (residents leaving), or both. |
| `limit`     | int    |          | `20`    | Top N connected tracts to return.                                   |

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/commuter-flows?tract=06075010800&direction=inbound&limit=10"
```

**Response**

```json theme={null}
{
  "tract": "06075010800",
  "inbound": {
    "total_workers": 14200,
    "flows": [
      { "origin_tract": "06081611100", "workers": 890, "pct": 6.3, "origin_name": "Daly City" },
      { "origin_tract": "06001402100", "workers": 720, "pct": 5.1, "origin_name": "Oakland" }
    ]
  },
  "outbound": {
    "total_residents_working": 3400,
    "flows": [
      { "dest_tract": "06075017902", "workers": 210, "pct": 6.2, "dest_name": "SoMa" }
    ]
  }
}
```

| Field                              | Type  | Description                              |
| ---------------------------------- | ----- | ---------------------------------------- |
| `inbound.total_workers`            | int   | Total workers commuting into this tract. |
| `inbound.flows`                    | array | Top origin tracts with worker counts.    |
| `outbound.total_residents_working` | int   | Total residents who commute out.         |
| `outbound.flows`                   | array | Top destination tracts.                  |

## `GET` `/api/v1/spatial-diagnostics`

<Note>Requires Bearer token.</Note>

Identify spatial score outliers using Local Moran's I (LISA) statistics and topological data analysis. Use this to find cells that behave differently from their neighbors — high-scoring cells surrounded by low scores, or vice versa.

**Query parameters**

| Name         | Type   | Required | Default | Description                                                                                                                        |
| ------------ | ------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `metro_slug` | string |          |         | Filter by metro area.                                                                                                              |
| `type`       | string |          |         | Spatial cluster type: `HH` (High-High), `HL` (High-Low), `LH` (Low-High), `LL` (Low-Low), `TDA_DONUT` (topological donut pattern). |
| `limit`      | int    |          | `50`    | Max results (max 500).                                                                                                             |

**Cluster types explained:**

| Type        | Meaning                               | Use case                                                                            |
| ----------- | ------------------------------------- | ----------------------------------------------------------------------------------- |
| `HH`        | High score surrounded by high scores  | Established strong markets.                                                         |
| `HL`        | High score surrounded by low scores   | Potential anchor locations driving change.                                          |
| `LH`        | Low score surrounded by high scores   | Underperforming cells in strong markets — possible opportunities.                   |
| `LL`        | Low score surrounded by low scores    | Weak market clusters.                                                               |
| `TDA_DONUT` | Ring of activity with a hollow center | Emerging development patterns where activity surrounds but hasn't reached the core. |

When no `type` is specified, the endpoint returns `HL` and `LH` outliers — the most analytically interesting patterns.

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/v1/spatial-diagnostics?metro_slug=sf&type=HL&limit=10" \
  -H "Authorization: Bearer al_your_key_here"
```

**Response**

```json theme={null}
{
  "outliers": [
    {
      "h3_index": "882a100d63fffff",
      "metro_slug": "sf",
      "score_baseline": 72,
      "experimental_composite": 78,
      "spatial_diagnostics": {
        "cluster_type": "HL",
        "local_moran": 2.34
      }
    }
  ]
}
```

| Field                                         | Type   | Description                                                                                   |
| --------------------------------------------- | ------ | --------------------------------------------------------------------------------------------- |
| `outliers`                                    | array  | Cells matching the spatial pattern, sorted by statistical significance.                       |
| `outliers[].score_baseline`                   | number | Current composite score.                                                                      |
| `outliers[].spatial_diagnostics.cluster_type` | string | LISA cluster classification.                                                                  |
| `outliers[].spatial_diagnostics.local_moran`  | number | Local Moran's I statistic. Higher absolute values indicate stronger spatial outlier behavior. |

## `GET` `/api/backtest`

<Note>Requires authentication. Session-based — available through the dashboard.</Note>

Validate scoring model accuracy by comparing historical predictions against outcomes. Requires at least 90 days of score history and 5 or more monitored locations.

**Example**

```bash theme={null}
curl "https://axiomlocus.io/api/backtest" \
  -H "Authorization: Bearer al_your_key_here"
```

**Response (ready)**

```json theme={null}
{
  "status": "ready",
  "data_collection_start": "2026-01-15",
  "days_of_data": 94,
  "metrics": {
    "r_squared": 0.82,
    "hit_rate": 0.76,
    "miss_rate": 0.68,
    "sample_size": 12
  }
}
```

**Response (insufficient data)**

```json theme={null}
{
  "status": "insufficient_data",
  "days_until_ready": 42,
  "days_of_data": 48,
  "data_collection_start": "2026-03-01",
  "sample_size": 3
}
```

| Field                 | Type   | Description                                                               |
| --------------------- | ------ | ------------------------------------------------------------------------- |
| `status`              | string | `ready` or `insufficient_data`.                                           |
| `metrics.r_squared`   | number | Correlation between predicted and actual scores (0–1).                    |
| `metrics.hit_rate`    | number | Fraction of high-scored locations (above 70) that maintained or improved. |
| `metrics.miss_rate`   | number | Fraction of low-scored locations (below 30) that stayed low or declined.  |
| `metrics.sample_size` | number | Number of location pairs used in the analysis.                            |
