# NCUA Credit Union Enforcement Watch (`titan_coder/ncua-credit-union-enforcement-watch`) Actor

Tracks NCUA credit union enforcement orders (cease & desist, prohibition, civil money penalty) by state or institution name, and alerts only on genuinely new or changed administrative actions — built for compliance officers, vendor risk teams, and credit union consultants. Free when nothing changes.

- **URL**: https://apify.com/titan\_coder/ncua-credit-union-enforcement-watch.md
- **Developed by:** [Radu Furtuna](https://apify.com/titan_coder) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 new enforcement action detecteds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## NCUA Credit Union Enforcement Watch

Durable monitor for the NCUA's public **Administrative Orders** registry — the National Credit Union
Administration's list of enforcement actions against credit unions and their affiliated individuals. Watch
by state and/or credit union name, and get notified only about actions genuinely new or changed since your
last check. No API key needed, no login, no captcha.

### Source

`https://ncua.gov/sites/default/files/list_csv/administrative-orders.csv` — a public CSV export of the
entire registry (1,443 rows spanning 1991–2026, confirmed live 13.09.2026), reachable directly from the
"Download CSV" button on `https://ncua.gov/news/enforcement-actions/administrative-orders`. One HTTP GET
returns the whole registry; there is no server-side filter by state or name for this export, so
`state`/`creditUnionNameContains` are applied on our side over one shared snapshot per run.

Each row names either a **credit union itself** (`subjectType: "institution"`, e.g. a Cease and Desist
Order against the institution) or a **named individual** affiliated with one (`subjectType: "individual"`,
e.g. a former employee or officer) — these are genuinely different kinds of record and are never merged.

### How it works

1. Each `watch` is an optional `state` (2-letter code) and/or `creditUnionNameContains` (case-insensitive
   substring match against institution/individual names) — or neither, to watch the entire registry.
2. Every run downloads the CSV once (cached for the whole run, regardless of how many watches you define)
   and filters it per watch.
3. A stable identity is built per row from its docket number, its unique registry URL, and a content hash
   of its substantive fields (institution, name, city, state, year, relationship). If NCUA later corrects
   a published row (e.g. a city name), the content hash changes and it is honestly treated as an update,
   not silently ignored or double-billed for no reason; if the exact same row reappears unchanged, nothing
   is re-billed.
4. Genuinely new or changed rows are pushed to the dataset and billed once each
   (`new-enforcement-action-detected`); a check that finds nothing new costs nothing beyond the fixed
   platform run cost.

### Input

```json
{
  "monitorId": "my-credit-union-watch",
  "userAgentContact": "you@example.com",
  "watches": [
    { "watchId": "louisiana", "state": "LA" },
    { "watchId": "acme-fcu", "creditUnionNameContains": "Acme Federal Credit Union" }
  ],
  "notifyOn": "new_alerts",
  "webhookUrl": "https://example.com/webhook"
}
```

`userAgentContact` is sent as a descriptive `User-Agent` on every request (polite-scraping practice) —
ncua.gov does not require a key, but we still identify ourselves.

Add more watches later under the same `monitorId` — each watch keeps its own independent history. A
`watchId` is permanently bound to the filter it first saw; pointing the same `watchId` at a different
`state`/`creditUnionNameContains` later fails the run instead of silently mixing histories.

### Output row (per action)

`watchId, stableRowId, docketNumber, subjectType ("institution"|"individual"), institution, firstName,
lastName, city, state, year, relationship, url, monitorId, runId, discoveredAt, eventId, billed`

### Billing

Pay-per-event: `new-enforcement-action-detected` — charged only for a row genuinely new or changed since
the previous check of that watch. The first check of a new watch establishes a baseline (no charge).
Failed/blocked checks are never charged.

#### Delivery/billing guarantee: **at-most-once**, not exactly-once

The right to perform an irreversible action (dataset write + PPE charge) is granted by the only atomic
primitive Apify offers — `RequestQueue.addRequest(uniqueKey)` → `wasAlreadyPresent` — in a separate
named queue that acts as a permanent journal (`<prefix>-<monitorId>-claims`). The Apify Key-Value Store
has no CAS, no conditional write and no ETag, so it can only ever be a diagnostic state machine, never
the source of at-most-once.

Concretely: for one computed event, delivery and charging each happen **no more than once**. If the run
dies *after* taking the claim, the event may be **lost** (it stays `dataset_unknown`/`charge_unknown`
and is never re-delivered) — but you will never be billed twice. That is a deliberate trade: "never
overcharge" beats "never lose a row".

Boundaries of the guarantee, stated honestly:

- Between the internal lease check and the dataset write/charge there is an unavoidable TOCTOU gap; what
  actually protects your money is the claim gate, not the lease.
- The guarantee holds for as long as the named claims queue exists. Anyone with account access can
  delete or recreate it via Console/API, which starts the journal from zero. This is a boundary of any
  durable storage, not a defect of the protocol.
- The guarantee applies from the build in which the claim gate was introduced onward. Older builds must
  not keep running against the same `monitorId`.

### Important — read before relying on this for compliance decisions

**NCUA itself states that its administrative orders list is not guaranteed to be exhaustive**, that names
reflect the organization's name at the time the order was finalized (and may no longer match the
organization's current name after a merger, rename, or liquidation), and that some prohibitions/orders may
have already expired or been lifted. **This actor is an informational monitor of NEW or CHANGED publications
to the official registry — it is NOT a current compliance/risk status for any organization, and it is NOT
legal advice.** Always confirm directly with NCUA (or counsel) before acting on any single entry.

### Honest limits

- **The durable dataset is a delivery-attempt log, not a guaranteed mirror of the default dataset.**
  Each row is written to the durable dataset first, then mirrored to the run's default dataset before
  billing proceeds for that row. If the durable write succeeds but the default-dataset mirror write
  fails (e.g. transient Apify storage error), the item is marked `dataset_unknown`, billing for it is
  permanently blocked (fail-closed — we never charge for a row we can't confirm was delivered), and the
  run is not retried into re-creating that exact row. The durable dataset can therefore end up with a
  small number of orphan rows that were never mirrored and never billed. The **default dataset is the canonical log of rows successfully written to this
  run's output** (see its `run_summary` row) — but a default-dataset row does not by itself prove the
  row was billed: the row is written before `Actor.charge()` runs, so if charging then fails or comes
  back `charge_unknown`, the row is present but not confirmably paid. **`run_summary.eventsBilled` and
  Apify's own billing ledger are the source of truth for confirmed payment**, not the presence of a row
  in either dataset.
- **No server-side filter exists for this CSV export** (verified live) — `state`/`creditUnionNameContains`
  are always applied client-side over one shared snapshot per run; they never reduce the actual network
  cost, only the rows delivered to you.
- Each watch's window is capped at `MAX_ROWS_PER_WATCH` (500) matching rows, newest-first (the source
  itself returns newest-first). `coverage.windowFullCount` and each watch's row in `coverage.watches`
  (`windowFull`/`matchedCount`) flag when a watch's real match count exceeded that window — informational,
  not silently dropped data. `coverage.sourceAccessLimitedCount` is a separate, unrelated counter: it only
  counts watches where the CSV itself could not be trusted this run (malformed rows or rows missing a
  docket/URL — see below), never a watch that simply had more matches than the window.
- Row-level identity depends on the registry always supplying a non-empty docket number and a unique URL
  for every row — both true for all 1,443 rows verified live on 13.09.2026. If any row is missing either
  (or has a malformed column count), the whole run for that check is reported honestly as
  `source_access_limited` — no partial baseline/delta/billing happens on a fetch whose completeness can't
  be trusted, rather than silently skipping just that one row.
- The source uses **two different conventions** for institution-only orders — verified live: sometimes
  `First Name = "N/A"` and the credit union name sits in `Last Name`, sometimes the reverse. Both are
  detected; only when *both* name fields would be empty does `subjectType` fall back to `"institution"`
  with a `null` name (not observed in the live registry as of 13.09.2026, but handled honestly rather than
  guessed at).
- History is kept per row (not a capped seen-ids list): each row's own state (its latest content hash and
  a change-sequence counter) is stored indefinitely per watch, so a row that changes and later reverts to
  an earlier state (e.g. corrected then re-corrected) is billed correctly every genuine transition, never
  silently deduplicated against an older occurrence of the same content.
- We don't invent data: if the CSV's header or row shape changes, the run reports it honestly
  (`structure_changed: ...`) instead of silently returning zero results.

Author: OmniCoder (https://t.me/OmniCoder)

# Actor input Schema

## `monitorId` (type: `string`):

Name of this monitor's durable history (a-z, 0-9, dash; up to 40 chars).

## `userAgentContact` (type: `string`):

Sent as a descriptive, identifiable User-Agent on every request to ncua.gov (polite-scraping practice) — ncua.gov does not require an API key, but we still identify ourselves. Give your email or a contact link. Not a secret.

## `watches` (type: `array`):

1-20 objects: {"watchId": "my-watch", "state": "LA" (optional, 2-letter state code), "creditUnionNameContains": "..." (optional, case-insensitive substring match against institution/credit union name OR the named individual on individual-subject orders)}. Leave both filters empty to watch the entire registry. New watches can be added later under the same monitorId.

## `notifyOn` (type: `string`):

new\_alerts — post the webhook only when new/changed paid enforcement actions were delivered; always — post it every run; never — do not call webhookUrl at all.

## `webhookUrl` (type: `string`):

Optional. Receives a digest of delivered (paid) new/changed enforcement actions as JSON. HTTPS only.

## Actor input object example

```json
{
  "monitorId": "my-credit-union-watch",
  "userAgentContact": "you@example.com",
  "watches": [
    {
      "watchId": "louisiana",
      "state": "LA"
    }
  ],
  "notifyOn": "new_alerts"
}
```

# Actor output Schema

## `results` (type: `string`):

Every row this run produced. Key fields: watchId, docketNumber, subjectType, institution, city, state, year, url.

## `coverage` (type: `string`):

What this run actually covered and what it charged for: per-watch status and reason, actions delivered and billed, and whether the registry snapshot had to be truncated for any watch. Enough to reconcile every charge against every row.

## `digest` (type: `string`):

A short human-readable summary of what this run found, written every run.

# 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 = {
    "monitorId": "my-credit-union-watch",
    "userAgentContact": "you@example.com",
    "watches": [
        {
            "watchId": "louisiana",
            "state": "LA"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("titan_coder/ncua-credit-union-enforcement-watch").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 = {
    "monitorId": "my-credit-union-watch",
    "userAgentContact": "you@example.com",
    "watches": [{
            "watchId": "louisiana",
            "state": "LA",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("titan_coder/ncua-credit-union-enforcement-watch").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 '{
  "monitorId": "my-credit-union-watch",
  "userAgentContact": "you@example.com",
  "watches": [
    {
      "watchId": "louisiana",
      "state": "LA"
    }
  ]
}' |
apify call titan_coder/ncua-credit-union-enforcement-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,titan_coder/ncua-credit-union-enforcement-watch"
        }
    }
}
```

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/2GMH5yjgcnUt4XoNU/builds/pqTrCxkulZN4YeDHW/openapi.json
