# Chicago, LA, Seattle & Austin Permit Monitor (`felixgoldwin/commercial-permit-change-monitor`) Actor

Track commercial building permits from official Chicago, Los Angeles, Seattle and Austin data. Compare saved snapshots for status and valuation changes within fixed issue-date windows. Includes source links and conservative commercial classification; no homeowner contacts.

- **URL**: https://apify.com/felixgoldwin/commercial-permit-change-monitor.md
- **Developed by:** [Felix GoldWin](https://apify.com/felixgoldwin) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 permit evidence rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Commercial Permit Change Monitor

Monitor conservatively classified commercial building-permit evidence in Chicago, Seattle, Austin, and Los Angeles. Each run reads bounded official public APIs, reports source coverage, and can compare retained fields with a caller-supplied snapshot from an earlier matching scan.

This package contains source code, Apify schemas, deterministic fixtures/tests, example inputs, and publishing instructions. Consult [QA.md](QA.md) for the actual validation results and live-check evidence. Apify account deployment, platform runs, billing configuration, and publication have not been performed. A source package is not proof of a successful platform run.

### Supported scope

| Source ID | Official dataset and trade | Commercial classification |
| --- | --- | --- |
| `chicago` | [Chicago Building Permits](https://data.cityofchicago.org/Buildings/Building-Permits/ydr8-5enu): new-construction and renovation/alteration permit types | Explicit supported commercial-use terms in the published work description, without supported residential/mixed-use exclusions. Negated, former-use, and conversion wording is unknown. This is text evidence, not a zoning determination. |
| `seattle` | [Seattle Building Permits](https://data.seattle.gov/Permitting/Building-Permits/76t5-zqzr): `Building` type | Requires both `Commercial` permit class and `Non-Residential` mapping. Institutional, industrial, missing, and conflicting classifications remain unknown. |
| `austin` | [Austin Issued Construction Permits](https://data.austintexas.gov/Building-and-Development/Issued-Construction-Permits/3syk-w9eu): `BP` building permits | Requires Commercial mapping plus a reviewed nonresidential class. Residential/multifamily classes remain excluded even if the source maps them Commercial. Reviewed source classes include certain institutional, religious, school, public-works, and industrial buildings; this is not a private-sector-only filter. |
| `los-angeles` | [Los Angeles Building Permits Issued from 2020 to Present](https://data.lacity.org/d/pi9x-tg5x): `Building` group with Commercial subtype | Uses the source's explicit Commercial subtype; residential conflicts and mixed-use evidence are unknown. The source reports a weekly refresh. Work descriptions and contractor companies are omitted. |

These are source-specific adapters for the general-building trade. Classification rules deliberately leave some records unknown; they are not a universal land-use classifier. The dataset contains only qualifying records and changes involving previously qualifying records. Per-city diagnostics count the residential, unknown, and out-of-scope records examined within that adapter's query. Los Angeles filters Commercial subtype on the server, so its diagnostics do not count every residential permit in the city.

The date range filters **permit issue date**, inclusive at both ends. Records with missing issue dates do not enter that range. This is not a feed of all modifications made during the date range: an old permit can change today and will be seen only if its issue date is inside the chosen window. Issuance does not establish a construction start. A record disappearing from a matching scan does not establish cancellation.

### Input

The prefilled form and Store demonstration use a small, fixed historical Chicago window:

```json
{
  "sources": ["chicago"],
  "dateFrom": "2026-10-02",
  "dateTo": "2026-10-02",
  "maxSourceRecords": 50,
  "maxResults": 10,
  "maxRequests": 8,
  "maxRunSeconds": 60,
  "maxChargeUsd": 0.02
}
```

This demonstration reads genuine published permits; it is not a current-events window. Change the dates deliberately for your monitoring scope. The complete effective prefill is [examples/store-prefill-input.json](examples/store-prefill-input.json); [QA.md](QA.md) records the actual local SDK result. Historical source records can still be corrected or removed, so future nonempty results are not guaranteed. See also [examples/input.json](examples/input.json) and [examples/four-cities-input.json](examples/four-cities-input.json). The input schema is [.actor/input_schema.json](.actor/input_schema.json). Unknown input keys, unsupported sources, duplicate sources, invalid dates, and unsafe nested JSON are rejected.

The table below describes runtime defaults **when a field is omitted**, including direct API calls with no supplied dates. Schema prefills shown above take precedence in the initial Console/Store input. They do not alter the runtime's omitted-date behavior.

| Input | Runtime default when omitted | Allowed limit / meaning |
| --- | --- | --- |
| `sources` | `["chicago"]` | One to four unique IDs from the table above; processed in sorted order. |
| `dateFrom`, `dateTo` | Seven days before the current UTC date, through today | Real ordered `YYYY-MM-DD` values, at most 367 inclusive days. Source issue dates retain their calendar meaning. Fix both dates for comparison runs. |
| `maxSourceRecords` | 500 | 1–1,000 rows per city, including nonqualifying rows in the adapter query. An excessive initial count stops that city before downloading pages. |
| `pageSize` | 100 | 1–250; sequential pages ordered by source permit identifier. |
| `maxResults` | 300 | 1–1,000 total evidence rows, including baseline, unchanged, and disappearance observations. |
| `maxRequests` | 32 | 1–100 across all cities, retries, metadata, and count requests. A successful city scan normally needs four verification requests plus its data pages. |
| `maxRunSeconds` | 120 | 5–300 for source retrieval. SDK startup and dataset/storage writes are additional. |
| `requestTimeoutSeconds` | 15 | 1–30 per attempt, including streaming the response. |
| `maxResponseBytes` | 2,000,000 | 10,000–5,000,000 per response, measured while streaming. |
| `maxTotalBytes` | 16,000,000 | 10,000–40,000,000 across responses. |
| `maxRetries` | 2 | 0–3 additional attempts for transient network, 429, and server errors. Access denials are not retried. Retry-After above ten seconds, or beyond the deadline, stops the scan. |
| `maxChargeUsd` | 0.05 | 0–10; conservative custom-event budget, with configured automatic start charges reserved and the platform event limit also consulted. It does not set a platform compute/storage budget. |
| `previousSnapshot` | Omitted | Complete same-scope snapshot JSON from the previous run; at most 1,000 records. Overall input is capped at 3 MB and accepted nesting depth is bounded. |

No source URL, proxy, credentials, or external snapshot-storage key is accepted. Source requests use only the hardcoded registry; redirects fail. The Actor does not log into a portal or follow record-detail links.

### Outputs and comparison

The default dataset contains one evidence row per retained permit observation, plus applicable observations for prior records no longer qualifying or no longer found. Each row contains `sourceId`, `permitId`, `event`, `before`, `after`, `changedFields`, `classificationAfter`, `observedAt`, and `interpretation`.

| `event` | Meaning |
| --- | --- |
| `baseline` | The source has no supplied previous snapshot. |
| `newlyObserved` | The permit was not in the matching supplied snapshot. It may be an older or corrected permit. |
| `unchanged` | Every retained normalized field equals the prior record. |
| `changed` | One or more retained fields changed; `changedFields` lists them. |
| `noLongerCommerciallyClassifiable` | A prior qualifying permit is still observed in the query, but no longer passes classification. |
| `noLongerObserved` | A prior qualifying permit is absent from a complete matching scan. This is not a cancellation claim. |

Each non-null `before` or `after` object contains the same retained record fields: source identifier, permit number, project address, work description when retained, permit/work type, published status, published milestone when available, issue date, source value, USD currency, retained contractor companies, classification evidence, and provenance. Provenance includes the source, official record link, and field names. Missing values stay null or empty arrays. Reported/estimated project cost is not contract value or confirmed spending.

The before/after records are exact copies of the **normalized fields retained by this Actor**, not full raw source rows. Contact patterns are redacted, dates can be reduced to a calendar date by an adapter, and numeric strings converted to numbers. A source field exceeding its retained text bound makes the city scan incomplete instead of silently comparing truncated text. Unselected source columns cannot produce changes. The Actor does not reconstruct changes that occurred between scans. `previousEvidenceOrigin` labels any before record as caller-supplied: syntax and scope are validated, but authenticity cannot be established.

The default key-value store contains:

- **`OUTPUT`**: `complete`/`incomplete`/`failed` status, scope, request/byte/retry counts, per-city scan and output flags, stop reasons, configured limits, pricing budget information, and reusable source IDs.
- **`SNAPSHOT`**: versioned source snapshots with a scope hash, complete flag, and retained records. Snapshot JSON is capped at 2 MB.

For the next comparison, copy the prior `SNAPSHOT` JSON into `previousSnapshot` and keep the same source IDs, adapter version, classification rule, and fixed issue-date range. A changed date window must start a new baseline. Do not paste `OUTPUT` or dataset rows as a snapshot. The Actor validates the prior snapshot, rejects duplicate records and untrusted provenance URLs, and reads no global cross-user state. The caller controls which snapshot is supplied and retained.

### Complete and incomplete scans

Before each city scan, the Actor reads dataset metadata and a bounded-query count. It requests the expected pages, checks their lengths and unique IDs, and reads the count and update marker again. A schema error, changing count/update marker, missing page, access denial, or budget limit prevents that city's changes and reusable snapshot from being emitted.

`complete` means the bounded query passed these checks. The public APIs do not provide a transactional historical snapshot, so these checks cannot prove there were no intermediate edits that left the checks unchanged. Source observation time is not source event time.

If all of a city's evidence rows cannot fit within the result/charge capacity, its planned output and snapshot are withheld; other complete cities can still be returned. This per-city planning does not make Apify dataset writes transactional. A later storage or billing error can leave partial delivered rows; check `OUTPUT`, do not reuse a failed snapshot, and do not interpret a missing row as a project event.

### Privacy and source restrictions

No homeowner, owner, applicant, personal phone, or personal email column is requested. Chicago's server projection retains only general-contractor names with an explicit corporate suffix. Seattle and Austin select their published contractor-company field and retain only corporate-suffix names; ambiguous personal/trading names are omitted. This heuristic is not corporate-registry verification and may omit legitimate businesses.

Chicago, Seattle, and Austin descriptions use best-effort contact redaction. Public free text can still contain names or unexpected personal details; review it before external redistribution. Los Angeles work descriptions are never requested or retained. See [NOTICE.md](NOTICE.md) for source attribution and terms.

### Local verification and deployment

Use Node.js 22 or later. From the extracted source root:

```sh
npm ci --ignore-scripts
npm test
npm run validate
npm audit --omit=dev
```

For a local source run, put an example input in `storage/key_value_stores/default/INPUT.json`, then run `npm start`. Save a reusable snapshot outside temporary SDK storage before a subsequent run. A source run contacts public APIs; tests use deterministic fixtures. `npm run smoke` performs the documented small public-source smoke check. `node scripts/store-prefill-smoke.js` derives input directly from the schema and runs the actual local SDK entry point in isolated temporary storage, without account credentials. It validates nonempty output rows and snapshot round trips and writes bounded evidence under `evidence/`. [QA.md](QA.md) records what was actually run and what remains unverified.

The Docker configuration uses Node.js 22, a 128 MB V8 heap target, and a fixed 256 MB Apify memory allocation. A V8 heap setting is not a process-memory guarantee; platform feasibility still needs its own run. Dependencies pin Apify SDK 3.7.2 and override the transitive `basic-ftp` package to 6.2.3. FTP is not used by these adapters; the override crosses a major version and does not constitute FTP-path testing.

Follow [PUBLISHING.md](PUBLISHING.md) to import the source package, verify the first platform run, and configure the owner-approved listing rates: $0.005 per automatic start unit and $0.001 per delivered permit evidence row. No prices are configured by this package.

### Hosted verification update — 2026-10-12 UTC

This dated update supersedes the original builder-only platform status above. The complete final v2 package was imported and its 52-file source tree verified against the approved archive after the Console’s outer-whitespace normalization. Apify build 0.0.2 succeeded with Node 22 at the package’s fixed 256 MB memory setting.

Two owner-account runs using the schema-generated historical Chicago demonstration (issue date 2026-10-02, max 50 source records, 10 result rows, 8 HTTP attempts, 60 source-work seconds and a $0.02 input event cap) each succeeded in about four seconds with five delivered rows. The platform run cap was $0.02 with a 120-second timeout and 256 MB memory. The baseline examined 24 published source rows and retained five commercial observations, used five requests and 442,698 response bytes, and reported complete scan/output with a reusable snapshot. The repeat accepted the baseline snapshot and reported all five observations unchanged. Peak memory across the Chicago tests was 52.5 MB; this is not a maximum-load guarantee.

Runtime confirmed pay-per-event mode with a $0.005 automatic start and $0.001 permit-evidence-row price. Baseline and unchanged observations are billable at the configured rate. The synthetic paid dataset-item event is removed. The unchanged runtime checks per-row event count increases before continuing. The owner’s Console rounded each run’s platform usage to $0.000; these owner runs are not customer invoice settlement tests. Customer pay-per-event prices include platform usage. A separate bounded hosted four-city run over the same issue-date window succeeded in 24 seconds with 54 rows: Austin 18, Chicago 5, Los Angeles 21, Seattle 10. It used 20 requests with no retries, 1,048,861 response bytes, and a 54 MB peak. All four diagnostics reported complete scan/output and reusable snapshots. This run used a $0.10 platform/input cap, max 100 source rows per city, 60 result rows, 32 requests and 90 source-work seconds. It is a small dated source check, not a guarantee of future source availability or maximum-load performance.

The platform-generated example is an historical demonstration, not a current construction feed. Do not infer construction starts, cancellation, guaranteed leads or complete city coverage from the retained observations.

A start-only platform cap of $0.005 was verified to produce zero source requests, zero rows and no reusable snapshot. A separate maxResults: 1 run with the prior Chicago snapshot completed its five-request scan, then withheld the entire five-row city because it could not fit; outputComplete was false, no rows were delivered and no snapshot was reusable. These limits did not create disappearance evidence. The saved Console example is restored to the bounded historical Chicago baseline with no previous snapshot, a $0.02 platform cap, 120-second timeout and 256 MB memory.

# Actor input Schema

## `sources` (type: `array`):

Hardcoded supported public sources. Only conservative commercial general-building permits are emitted.

## `dateFrom` (type: `string`):

YYYY-MM-DD. The prefilled 2026-10-02 date is a fixed historical Chicago demonstration verified against the public source. If omitted through the API, runtime defaults to seven days before the run. Keep identical dates between compared scans.

## `dateTo` (type: `string`):

YYYY-MM-DD. The prefilled 2026-10-02 date is a fixed historical demonstration, not a current-events window. If omitted through the API, runtime defaults to the current UTC day. Source dates retain local calendar meaning. Span at most 367 days.

## `previousSnapshot` (type: `object`):

Paste the previous run SNAPSHOT JSON; same city/version/date window only. No URLs, external store keys or credentials. Maximum 1000 records and overall input 3MB.

## `maxSourceRecords` (type: `integer`):

Includes residential and unknown rows checked for conservative classification. A larger source count produces no city changes; narrow dates.

## `pageSize` (type: `integer`):

Sequential pages, no concurrent requests.

## `maxResults` (type: `integer`):

Includes baseline, unchanged, changed and no-longer-observed evidence. City delivery is atomic; a city that will not fit is withheld.

## `maxRequests` (type: `integer`):

Global across cities and retries; metadata/count requests included.

## `maxRunSeconds` (type: `integer`):

Bounds source retrieval; dataset writes and SDK startup are additional.

## `requestTimeoutSeconds` (type: `integer`):

Includes response body streaming.

## `maxResponseBytes` (type: `integer`):

Per public HTTP response; decompressed streaming size enforced.

## `maxTotalBytes` (type: `integer`):

Across all source responses in the run.

## `maxRetries` (type: `integer`):

429 and server/network errors only; access denials never retried. Retry-After above 10 seconds stops.

## `maxChargeUsd` (type: `number`):

Additional conservative PPE event cap including automatic start fees; platform cap also applies. Zero stops source work. Does not control platform compute/storage charges outside PPE; set platform run limits too.

## Actor input object example

```json
{
  "sources": [
    "chicago"
  ],
  "dateFrom": "2026-10-02",
  "dateTo": "2026-10-02",
  "maxSourceRecords": 50,
  "pageSize": 100,
  "maxResults": 10,
  "maxRequests": 8,
  "maxRunSeconds": 60,
  "requestTimeoutSeconds": 15,
  "maxResponseBytes": 2000000,
  "maxTotalBytes": 16000000,
  "maxRetries": 2,
  "maxChargeUsd": 0.02
}
```

# Actor output Schema

## `evidence` (type: `string`):

No description

## `diagnostics` (type: `string`):

No description

## `snapshot` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [
        "chicago"
    ],
    "dateFrom": "2026-10-02",
    "dateTo": "2026-10-02",
    "maxSourceRecords": 50,
    "maxResults": 10,
    "maxRequests": 8,
    "maxRunSeconds": 60,
    "maxChargeUsd": 0.02
};

// Run the Actor and wait for it to finish
const run = await client.actor("felixgoldwin/commercial-permit-change-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sources": ["chicago"],
    "dateFrom": "2026-10-02",
    "dateTo": "2026-10-02",
    "maxSourceRecords": 50,
    "maxResults": 10,
    "maxRequests": 8,
    "maxRunSeconds": 60,
    "maxChargeUsd": 0.02,
}

# Run the Actor and wait for it to finish
run = client.actor("felixgoldwin/commercial-permit-change-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    "chicago"
  ],
  "dateFrom": "2026-10-02",
  "dateTo": "2026-10-02",
  "maxSourceRecords": 50,
  "maxResults": 10,
  "maxRequests": 8,
  "maxRunSeconds": 60,
  "maxChargeUsd": 0.02
}' |
apify call felixgoldwin/commercial-permit-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,felixgoldwin/commercial-permit-change-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/OMGBNypHi6N293UNN/builds/O1HwFg21olbWqzIli/openapi.json
