# CFPB Company Complaint Delta Monitor (`titan_coder/cfpb-company-complaint-delta`) Actor

Watches specific companies in the official CFPB Consumer Complaint Database and reports only complaints filed since your last check. Informational feed only, not a verified finding of wrongdoing. A check with nothing new is free.

- **URL**: https://apify.com/titan\_coder/cfpb-company-complaint-delta.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

$5.00 / 1,000 new complaint 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

## CFPB Company Complaint Delta Monitor

Durable monitor for new consumer complaints filed against specific companies, via the official, free
CFPB Consumer Complaint Database Search API (`consumerfinance.gov`). No API key needed.

### Important legal/factual disclaimer

**This is an informational feed of complaints as filed with the CFPB — not a verified finding of
wrongdoing, not investment advice, not a compliance conclusion.** The CFPB itself states that complaints
in this database are not verified: consumers submit them, the named company may have responded and
disputed the complaint, and publication can be delayed. A complaint appearing here means someone filed
it — nothing more. Do not represent this actor's output as evidence of misconduct. Not real-time: the
CFPB's own published data lags several days behind the current date (observed ~5-7 days live on
12.09.2026).

### How it works

1. Each `watch` is one company, identified by its **exact name as recorded in the CFPB database** (e.g.
   `"WELLS FARGO & COMPANY"`, `"EQUIFAX, INC."`) — check the exact spelling on
   `consumerfinance.gov/data-research/consumer-complaints/search/` first; a near-miss name silently
   returns zero complaints, it does not error.
2. Every run fetches that company's most recent complaints (`sort=created_date_desc`, up to 100 per
   request — the CFPB Search API returns a **window** of the newest complaints, not the full history in
   one response) and diffs `complaint_id` against a durable checkpoint of ids already seen for that watch.
3. Genuinely new complaints are pushed to the dataset and billed once each (`new-complaint-detected`);
   checking a company with nothing new costs nothing beyond the fixed platform run cost. The first check
   of a new watch establishes a baseline (no charge) — you get a free look at the current top of the
   complaint stream, then pay only for what's genuinely new afterward.
4. Optional `product`/`issue`/`state` filters narrow the complaint stream server-side for a given watch
   (e.g. only "Credit card" complaints from "CA").

### Input

```json
{
  "monitorId": "my-companies",
  "watches": [
    { "watchId": "wells-fargo", "company": "WELLS FARGO & COMPANY" },
    { "watchId": "equifax-ca-fraud", "company": "EQUIFAX, INC.", "issue": "Fraud or scam", "state": "CA" }
  ],
  "notifyOn": "new_alerts",
  "webhookUrl": "https://example.com/webhook"
}
```

Add more watches later under the same `monitorId` — each watch keeps its own independent history.

### Billing

Pay-per-event: `new-complaint-detected` — charged only for a complaint genuinely new 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 guarantee: at-most-once (not exactly-once)

The right to write a row and to charge for it is granted by a single atomic primitive — one
`addRequest(uniqueKey)` into a dedicated, named claim-journal Request Queue
(`<prefix>-<monitorId>-claims`). Exactly one run ever wins that key. Claim requests are never deleted
and never handled: the queue is a permanent journal of irreversible attempts, not a work list.

- **You will never be charged twice for the same complaint.** That is the guarantee.
- **It is not exactly-once.** If a run wins the claim and then dies before the row reaches the dataset
  (or before the charge completes), that complaint is *lost*: it closes as `dataset_unknown` /
  `charge_unknown` and is never re-delivered. We deliberately prefer losing a delivery over
  double-charging you.
- **Boundary of the guarantee:** it holds for as long as the named claim-journal queue exists. Anyone
  with account access can delete or re-create that queue through the Apify Console/API; a fresh journal
  starts empty, and previously delivered complaints could then be delivered and billed again. That is
  an inherent limit of any durable storage, not a defect of the protocol.
- **Migration boundary:** the guarantee applies from the build that introduced the claim gate onward.
  Older builds of this actor must not keep running against the same `monitorId`. That same build also
  had to shorten the durable storage name prefix (the old one, the full actor name, could not fit
  Apify's 63-character storage-name limit together with a 40-character `monitorId`), so a monitor that
  ran on an older build starts from a fresh baseline once. A baseline is never charged.
- `coverage.claimJournalSize` reports the journal's size each run (best-effort; `null` if the queue's
  metadata could not be read, and the value lags a few seconds because Apify's `totalRequestCount` is
  eventually consistent). Use it to watch growth, not to make decisions.

### Honest limits

- **Window, not full history.** The CFPB Search API returns up to 100 of the newest complaints per
  request; if a company has more new complaints since your last check than fit on one page,
  `coverage.windowFullCount` reports it honestly rather than silently dropping older-but-still-new
  complaints (they surface on the following run once the window catches up, same pattern as
  `federal-register-monitor`).
- **Exact company name required.** There is no fuzzy matching: a misspelled or slightly different
  company name returns zero complaints (a legitimate, non-error "nothing new" result), not an error. Use
  the CFPB's own search page to confirm exact spelling before configuring a watch.
- **Not real-time.** The CFPB itself publishes complaints with a delay (observed ~5-7 days on
  12.09.2026); this actor cannot see a complaint the CFPB has not yet published, no matter how often you
  run it.
- **We don't invent data.** If the CFPB API response shape changes or returns an unexpected status, the
  run reports it honestly (`permanent_error: ...` / `transient_error: ...`) instead of silently returning
  zero results. Fields not present in the live CFPB response (e.g. the historical `consumer_disputed`
  field, retired by CFPB some years ago) are not fabricated — we only surface fields confirmed present in
  a live response.
- `seenIds` per watch is capped (FIFO by discovery order, 2000 entries) — only matters for a company with
  an extraordinarily high complaint volume relative to check frequency.
- The `url` field on each delivered row links to the CFPB's own complaint-detail page
  (`.../search/detail/<id>`). That page is client-rendered (SPA) and returns HTTP 200 for the URL shape
  itself regardless of whether the id resolves inside the app — it is a best-effort deep link into the
  same UI the CFPB publishes, not a server-validated permalink.

### Deviations from the original brief (see also ROADMAP.md)

- The brief's example query included `format=json`. Live testing on 12.09.2026 showed this parameter
  makes the API return **HTTP 404** — the response is JSON by default without it, so we never send it.
- Live testing also found the API sits behind an Akamai WAF that returns **HTTP 403** for a plausible
  chunk of "app-looking" `User-Agent` strings (including our own actor name as a plain `name/version`
  string, and even `Mozilla/5.0`) while passing through strings that honestly self-identify as a bot/
  crawler (containing `bot`/`crawler`) or well-known tool UAs (`curl`, `Wget`, unmodified
  `python-requests`). We send `cfpb-company-complaint-delta-bot/0.1` — an honest self-identification,
  not UA spoofing — which passes reliably in live testing.
- The brief's example schema listed `consumer_disputed`. It is not present in the live API response
  (CFPB retired this field in the published dataset some years ago); we do not fabricate it.

No CFPB endpoint used here required an API key or authentication at any point during development.

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).

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

1-30 objects: {"watchId": "...", "company": "..."\[, "product": "...", "issue": "...", "state": "XX"]}. `company` must match the exact name as recorded in the CFPB database (e.g. "WELLS FARGO & COMPANY") -- check spelling on consumerfinance.gov/data-research/consumer-complaints/search/ first. `product`/`issue`/`state` optionally narrow the complaint stream for that company. New companies can be added later under the same monitorId.

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

new\_alerts — post the webhook only when new paid complaints 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 complaints as JSON. HTTPS only.

## Actor input object example

```json
{
  "monitorId": "my-companies",
  "watches": [
    {
      "watchId": "wells-fargo",
      "company": "WELLS FARGO & COMPANY"
    }
  ],
  "notifyOn": "new_alerts"
}
```

# Actor output Schema

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

Every row this run produced. Key fields: watchId, complaintId, product, issue, dateReceived, state, companyResponse. Informational only -- CFPB complaints are not verified findings of wrongdoing.

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

What this run actually covered and what it charged for: per-target status and reason, complaints delivered and complaints billed, window-full flags. 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-companies",
    "watches": [
        {
            "watchId": "wells-fargo",
            "company": "WELLS FARGO & COMPANY"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("titan_coder/cfpb-company-complaint-delta").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-companies",
    "watches": [{
            "watchId": "wells-fargo",
            "company": "WELLS FARGO & COMPANY",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("titan_coder/cfpb-company-complaint-delta").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-companies",
  "watches": [
    {
      "watchId": "wells-fargo",
      "company": "WELLS FARGO & COMPANY"
    }
  ]
}' |
apify call titan_coder/cfpb-company-complaint-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,titan_coder/cfpb-company-complaint-delta"
        }
    }
}
```

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/u186L3XIdTutdCJYd/builds/fOkvNWAx7spu0uKgI/openapi.json
