# NYC LL97 DOB NOW Penalty Status Monitor (`thatmike1/nyc-ll97-penalty-status`) Actor

Check NYC Local Law 97 DOB Safety Violations by BIN. Export published statuses, source IDs and before/after changes. Successful checks include unchanged and zero-match results.

- **URL**: https://apify.com/thatmike1/nyc-ll97-penalty-status.md
- **Developed by:** [Michal Pšenčík](https://apify.com/thatmike1) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.00005 / actor start

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## NYC LL97 DOB NOW Penalty Status Monitor

Check explicit NYC building identification numbers (BINs) against the public **DOB Safety Violations** dataset `855j-jady`, restricted to `device_type='GHG Emissions - LL97'`. Each successful check exports the complete bounded set of published records, including their source IDs, statuses and dates. An optional named watch reports before/after changes across delivered checks.

This is a source-record monitor. It does not determine Local Law 97 compliance, calculate emissions or fines, verify filings, or report payments or balances. A zero-match result does not prove a BIN exists, that a filing was submitted, or that a building is compliant. Related violation numbers remain separate source records; they do not establish separate legal liabilities.

### Input

The default input checks one real building:

```json
{ "bins": ["3180289"] }
```

For a portfolio with persistent comparison history:

```json
{
    "bins": ["3180289", "1015826", "1000003"],
    "watchName": "portfolio-a",
    "maxChecks": 100,
    "requestIntervalMs": 2000
}
```

| Field               | Meaning                                                                                                                                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bins`              | Required, 1–100 strings. Trimmed values must contain seven digits, starting with 1–5. Duplicates are checked once, in first-occurrence order; free SUMMARY retains every original index. Numeric BINs are rejected. |
| `watchName`         | Optional, null by default. Case-sensitive, 1–64 ASCII letters, digits, `_` or `-`. Omit for stateless checks.                                                                                                       |
| `maxChecks`         | Integer 1–100, default 100. Maximum successfully stored complete checks.                                                                                                                                            |
| `requestIntervalMs` | Integer 1,300–10,000, default 2,000. Idle time after each response; concurrency is one.                                                                                                                             |

Unknown properties, source URLs, filters, headers, keys and credentials are rejected. There is no address lookup or citywide discovery.

### Output

The default dataset contains **one billable receipt per complete stored BIN check**. Unchanged and zero-match checks are useful results and cost the same item fee. Failed or incomplete scans produce no dataset row.

Each receipt contains `schemaVersion`, `normalizationVersion`, `recordId`, `requestIndex`, `datasetId`, `bin`, `watchName`, `result`, `scanComplete`, `scanStartedAt`, `observedAt`, `sourceUpdatedAt`, `previousObservedAt`, `changeType`, `snapshotHash`, `records`, `changes`, `sourceUrl`, `sourcePageUrls`, and `limitations`. Nullable fields are always present.

`records` includes only `recordKey`, `bin`, `bbl`, `violationNumber`, `violationType`, `violationRemarks`, `violationStatus`, `deviceType`, `deviceNumber`, `violationIssueDate`, and `cycleEndDate`. BBL and identifiers remain strings. Source codes such as `FTF-EN-LL97320`, `FTF-EN-LL97321`, and `FTX-EN-LL97320` are preserved without inferred meanings. Status is an open string: observed examples include Active, Cured and Dismissed.

Source issue/cycle timestamps are timezone-free calendar strings, preserved with their original precision. No `Z` or UTC conversion is added. Observation and publication timestamps are UTC instants. Publication time is dataset-wide, not a record revision time.

The free default key-value store records are:

- `SUMMARY`: requested/unique/duplicate/attempted/completed/stored counts, available SDK charge counts, source requests/bytes, run status, per-original-index outcomes, errors, warnings and any retry recommendation.
- `OUTPUT`: a compact result and the explicit unfinished BINs to retry later.

Use the output links to retrieve these diagnostics. Errors are never disguised as empty successful checks. A failure before storage is free of item fees, but the run's start event and platform usage still apply.

### Watch behavior

The first delivered check of a BIN is `baseline`, without historical changes. Later checks are `unchanged` or `changed`, with sorted changes of kind `first_seen`, `updated`, `absent_from_source`, or `reappeared`. Changes contain full `before`/`after` records; updated changes list sorted normalized field names. Corrections and status regressions are visible without inferred legal progression.

Absent records remain bounded tombstones. Absence does not mean Cured or Dismissed; repeated absence does not generate another change. Stateless checks use `untracked` and have no change history.

Watch state uses the named store `ll97-watch-v1-` plus the first 40 hex characters of SHA-256(watchName), a deterministic 54-character name within Apify's 63-character limit. It advances only after dataset readback confirms the receipt. A post-delivery state failure fails the run while retaining the delivered row. A later run can repeat a change after a crash: observations are at least once. Same-run recovery checks the receipt ID before pushing again.

**Serialize all runs of a watch**, including partitioned portfolios. A best-effort lease detects many overlaps, but KV storage is not a transactional distributed lock. Undetectable simultaneous starts are unsafe. A changed normalization version requires a new watch name. Different names have independent histories; remove the corresponding named store when you no longer need that history.

### Pricing

Approved event prices are **$0.00005 per platform start unit** and **$0.002 per complete stored BIN check**, including baseline, unchanged and zero-match checks. **Platform usage is additional and paid by the user**; the publisher must enable and verify the supported PPE usage pass-through setting before public release.

At one start unit, event fees are $0.00205 for one check, $0.05005 for 25 checks, and $0.20005 for 100 checks. A run with no stored receipts has only the $0.00005 start event, plus usage. These examples exclude usage, discounts and taxes and are not measured invoices. Higher memory may increase start units. The official data API is free; the Actor charges for bounded retrieval, receipts and optional comparisons.

### Bounds and source access

Runtime requests use only the keyless HTTPS SODA v2 metadata and resource endpoints on `data.cityofnewyork.us`; no account, token, proxy, cookies, browser or challenge workaround is used. Each BIN query uses the exact LL97 filter, explicit `violation_number, :id` ordering, pages of 100 and a maximum of 500 raw rows plus an empty sentinel. Conflicting identities, wrong filters, malformed fields, missing/changing publication headers or stale publication flags fail that BIN without a partial receipt.

Limits per run: 250 HTTP attempts; 30 seconds per attempt; two transient retries with 2s/5s backoff plus normal idle pacing; 2 MiB per response; 16 MiB aggregate source traffic; 900 seconds of work plus up to 30 seconds for shutdown; 1 MiB per receipt; 100 dataset writes; 300 dataset reads; 300 KV reads/writes; 64 MiB internal traffic. Conservative resource reservations can end a run before all requested checks are delivered. Retry unfinished explicit BINs from SUMMARY; no partial cursor is saved.

Watch limits: 100 distinct retained BINs, 10,000 retained identities, 1 MiB per BIN state, 16 MiB per namespace. Retained capacity reservations do not shrink automatically after storage failures. Use another watch or explicitly repair/reset your history if its capacity is exhausted. Large before/after changes can exceed the receipt limit even when the current snapshot fits.

403, 429 and HTML/challenge responses stop the source origin for the run. Retry-After is recorded; the Actor never changes identity or routes around a block. 401/402 remain unavailable-access errors. Two consecutive BINs exhausting transient retries stop remaining work. Keyless shared datacenter egress may throttle sooner than desktop access.

Source: [NYC DOB Safety Violations](https://data.cityofnewyork.us/Housing-Development/DOB-Safety-Violations/855j-jady), [metadata](https://data.cityofnewyork.us/api/views/855j-jady.json), [public API](https://data.cityofnewyork.us/resource/855j-jady.json). NYC describes a daily publication of newer DOB NOW civil penalties; legacy datasets may overlap. Source consistency headers are a useful signal, not a transactional snapshot or a guarantee that the source is complete or current. The Actor does not query legacy DOB/OATH/ECB/BEAM systems or reconcile them.

Freeform remarks are exported as published. Address, owner, contact, geolocation and census fields are excluded; no personal information is enriched. The user is the controller for downstream processing, retention and sharing of any published personal data.

Public release remains gated on hosted Apify access, measured memory, configured usage pass-through, settled invoices and real spending-cap behavior. Local SDK tests do not prove platform billing. There is no independent paying-demand claim for this exact feed.

# Actor input Schema

## `bins` (type: `array`):

1–100 explicit seven-digit BIN strings starting with 1–5. Trimmed duplicates are checked once; original indexes appear in free SUMMARY.

## `watchName` (type: `string`):

Optional case-sensitive history namespace (1–64 ASCII letters/digits/\_/-). Omit for stateless runs. Serialize every run of the same watch.

## `maxChecks` (type: `integer`):

Caps complete stored BIN receipts, including unchanged and zero-match results.

## `requestIntervalMs` (type: `integer`):

Wait after each complete response. One request in flight; retries also obey this gate.

## Actor input object example

```json
{
  "bins": [
    "3180289"
  ],
  "maxChecks": 100,
  "requestIntervalMs": 2000
}
```

# Actor output Schema

## `rows` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `output` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("thatmike1/nyc-ll97-penalty-status").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("thatmike1/nyc-ll97-penalty-status").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 '{}' |
apify call thatmike1/nyc-ll97-penalty-status --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thatmike1/nyc-ll97-penalty-status"
        }
    }
}
```

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/77M4ryVBeV9PdgvRL/builds/pr9hSQ6FGS812c7oU/openapi.json
