WA Lobbyist Reporting Delta
Pricing
$10.00 / 1,000 new or amended lobbyist filing detecteds
WA Lobbyist Reporting Delta
Monitors Washington State PDC lobbyist registrations, expense reports, and amendments for journalists, compliance/govt-affairs teams, and watchdog orgs. Filter by entity, name, report type, or amendments only. Free when nothing changes.
Pricing
$10.00 / 1,000 new or amended lobbyist filing detecteds
Rating
0.0
(0)
Developer
Radu Furtuna
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
Durable monitor for Washington State's public Lobbyist Reporting History — the Public Disclosure Commission's (PDC) complete log of lobbyist/firm/client registrations, monthly expense reports, annual reports, and — critically — amendments to any of the above. Watch by entity, lobbyist/firm/client name, or report type, and get notified only about filings genuinely new since your last check. No API key needed, no login, no captcha.
Source
https://data.wa.gov/resource/nuwx-ay5h.json (Socrata SODA API), dataset "Lobbyist Reporting History"
(nuwx-ay5h), attribution "Public Disclosure Commission" — 201,431 filings confirmed live 13.09.2026, 435
new filings in the first 12 days of September 2026 alone. Unlike a small CSV export, this dataset is too
large (200K+ rows, many carrying a full JSON blob of the original form) to fetch in full every run — each
watch instead runs two lightweight SoQL queries: an honest count(*) under your filter, and a window of the
newest filings by receipt_date matching it (see Honest limits for what that window does and doesn't
cover).
Architecture: an append-only filing log, not a status registry
This registry does not work like "one license record whose status field changes." It is a log of
discrete filing events: an L1 registration, an L2 monthly expense report, an L3/L3c annual report — and
every single amendment to any of those — each gets its own permanent, never-reused id ("submission
version number", confirmed live via the dataset's own field documentation) and its own receipt_date. When
a filer amends a previously filed report, the amendment is not an edit of the old row: it is an
entirely new row (origin containing "AMENDED", amends_report pointing back at the original). This
means the amendment event is itself captured simply as "a new filing appeared" — the same mechanism that
catches a first-time registration. (The reverse link, amended_by_report, does get added to the old row
after the fact, but we don't need to re-poll old rows to detect the amendment — the new row is the signal.)
This is the same append-only shape as ncua-credit-union-enforcement-watch and
delaware-professional-disciplinary-actions, not the "one entity, mutable status" shape of e.g.
ny-attorney-bar-status-delta.
How it works
- Each
watchcombines optional filters —entityId(matchesentity_id,firm_id, orclient_id; the PDC's own stable identifier for a lobbyist/firm/client across years),filerNameContains(case-insensitive substring, applied server-side),reportTypes(subset ofL1_CLIENT|L1_CONTRACT|L1_FIRM|L2|L3|L3C|L5), andamendmentsOnly(only rows whoseorigincontains"AMENDED") — or none of them, to watch the newest filings across the entire registry. - Every watch runs its own
$where-filteredcount(*)and its own newest-first window (receipt_dateDESC,idDESC, capped atFETCH_WINDOW_SIZE= 1000 rows) — filters are applied server-side by Socrata, not downloaded-then-filtered. - A stable identity is the filing's own
id(never reused across the dataset's history) plus a content hash of its substantive fields. If the underlying data for an existingidwere ever corrected in place (not observed live, but not assumed impossible either), the hash would change and it is honestly treated as a revision, not silently ignored or double-billed. - Genuinely new (or, defensively, revised) filings are pushed to the dataset and billed once each
(
new-or-amended-filing-detected); a check that finds nothing new costs nothing beyond the fixed platform run cost.
Input
{"monitorId": "my-lobbyist-watch","userAgentContact": "you@example.com","watches": [{ "watchId": "gordon-thomas-honeywell", "entityId": 38721 },{ "watchId": "all-amendments", "amendmentsOnly": true },{ "watchId": "acme-client", "filerNameContains": "Acme Corp", "reportTypes": ["L1_CLIENT"] }],"notifyOn": "new_alerts","webhookUrl": "https://example.com/webhook"}
userAgentContact is sent as a descriptive User-Agent on every request (polite-scraping practice) —
data.wa.gov does not require it, but we still identify ourselves. An optional socrataAppToken raises
Socrata's own throttling limits if you have one; not required for normal use.
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 different filters
later fails the run instead of silently mixing histories.
Output row (per filing)
watchId, stableId, origin, isAmendment, filerName, filerType (Firm|Client|Public agency), entityId, firmId, clientId, year, receiptDate, filingMethod, reportFrom, reportThrough, amendsReport, amendedByReport, url, monitorId, runId, discoveredAt, eventId, billedBilling
Pay-per-event: new-or-amended-filing-detected — charged only for a filing genuinely new (or, defensively,
revised) 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.
Not the same product as existing WA lobbying actors
A prior Apify actor covering Washington lobbying (j0401/wa-lobbying) pulls from two different
datasets — "Lobbyist Compensation by Source" (9nnw-c693) and "PDC Enforcement" (a4ma-dq6s) — and bills
$0.003/record as a one-off snapshot (confirmed live against its own listing and OpenAPI schema
13.09.2026: no mention anywhere of monitor/delta/sinceLastRun/scheduled behavior). This actor watches the
Lobbyist Reporting History dataset (nuwx-ay5h) specifically, bills only newly-detected filings per
watch per run, and is built around amendments as a first-class, explicitly filterable signal
(amendmentsOnly) — a genuinely different source, filter, and billing model, not a repriced clone.
Important — read before relying on this for compliance decisions
The PDC's own dataset does not claim to be a real-time or complete record of lobbying activity in
Washington — filings are self-reported by lobbyists/firms/clients, paper filings (filingMethod: "Paper",
~4,500 of 201,431 rows as of 13.09.2026) carry no machine-readable reportData, and a separate 180 rows
are missing year/reportFrom/reportThrough entirely. This actor is an informational monitor of NEW
or AMENDED filings in the
official registry — it is NOT a compliance/risk verdict on any lobbyist, firm, or client, and it is NOT
legal advice. Always confirm directly with the PDC (or counsel) before acting on any single filing.
Delivery guarantee: at-most-once (we would rather lose an alert than bill you twice)
Each computed change is delivered to the dataset and charged at most once, for as long as the
monitor's claim log exists (see the boundary below). Before any irreversible step (writing the row,
charging the event) the run takes an atomic claim on that exact change, using the only atomic
primitive the Apify platform offers: a request queue's unique-key insert. Exactly one run can win that
claim for a given change. The claim log is never consumed, deleted or rotated by this actor; it is a
permanent record of what was already attempted, and coverage.claimJournalSize reports its size each run
so you can watch it grow (the platform's counter is eventually consistent, so treat it as a lagging
estimate, not an exact count).
Where that guarantee ends — the honest boundary. The claim log lives in a named request queue
(<prefix>-<monitorId>-claims) in your own account. The at-most-once guarantee holds as long as that
queue keeps existing. If you — or any process holding your account credentials — delete, rename or
re-create it from the Console or API, the log starts empty and previously delivered changes can be
delivered and charged again. That is the unavoidable boundary of any durable storage, not a loophole in
the protocol. For the same reason, the actor's storage prefix and internal claim namespace are frozen
after release: changing either would create a fresh, empty log with exactly the same effect.
The response the platform returns for each claim is interpreted strictly: only a real boolean false
grants the right to write and charge, only a real boolean true denies it, and anything else — a missing
field, null, 0, an empty string, a changed SDK response shape — aborts the run's delivery for that
item with claim_protocol_error before any row or charge. An answer we do not fully understand is
never read as "you may charge".
One thing we deliberately do not claim: the monitor's lease makes overlapping runs a fail-closed exception rather than a fact of life, but between the moment a run verifies it still holds the lease and the moment the dataset write or charge actually lands there is an unavoidable time gap (the platform offers no fencing token for datasets or billing). So "a run that lost the lease can never write another row" would be an overstatement. What actually protects your money is the claim above: the key is already taken, so even a ghost run cannot charge for the same change twice.
The honest consequence, stated plainly: if a run dies after taking the claim but before finishing, that
one change is lost. It is recorded as dataset_unknown or charge_unknown and it is not
re-delivered on the next run — the next run moves on to the watch's next new or amended filing. We deliberately chose possible loss
of one alert over the possibility of charging you twice for the same event. This is at-most-once
delivery, not exactly-once; any actor that claims exactly-once over a store without compare-and-swap is
overstating what the platform can do.
Practically this only happens if the Apify run is killed mid-delivery (platform abort, timeout, migration).
Every such case is visible: the run's coverage and run_summary report it, and run_summary.eventsBilled
plus Apify's own billing ledger remain the source of truth for what you actually paid for.
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 itsrun_summaryrow) — but a default-dataset row does not by itself prove the row was billed: the row is written beforeActor.charge()runs, so if charging then fails or comes backcharge_unknown, the row is present but not confirmably paid.run_summaryand Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset. - Each watch sees only a bounded window of the newest matching filings per run (
FETCH_WINDOW_SIZE= 1000, orderedreceipt_dateDESC thenidDESC).coverage.watches[].windowFullflags when a watch's real match count (from its owncount(*)) exceeded that window in this run — older matches beyond the window are honestly not shown this run, not silently dropped forever (the next run's window will still be newest-first, so a very high-volume unfiltered watch could in theory miss a burst of >1000 filings between two runs — narrow such a watch withentityId/filerNameContains/reportTypesif that matters to you).coverage.watches[].matchedCountis this watch's owncount(*)and may differ by a handful of rows from what the window actually returned, purely because the two queries are separate HTTP requests against a live, continuously-growing dataset (not a transactional snapshot) — this is not treated as a structural error. coverage.sourceAccessLimitedCountis a separate, unrelated counter fromwindowFullCount: it only counts watches where the Socrata response itself could not be trusted this run (malformed JSON, a non-numericid, a required field missing/blank, or a nullable field present as a whitespace-only string instead of a real null — see below), never a watch that simply had more matches than the window.- Row-level identity depends on every filing in the window supplying a non-empty, purely numeric
idand a non-emptyurl— both true for all 201,431 rows verified live on 13.09.2026.origin,filerName,filerType,receiptDate, andfilingMethodare likewise required and never null on any live row checked. If any row in a watch's window fails any of these checks, the whole run for that watch is reported honestly assource_access_limited— no partial baseline/delta/billing happens on a window whose completeness can't be trusted, rather than silently skipping just that one row. entityIdis not always populated, unlike its own field description suggests — discovered by a live smoke test while building this actor, not assumed from documentation: 11,876 rows (all) have nofilerType: "Public agency"entity_idat all, only aclient_id.entityId-filtered watches still match such rows viaclient_id; the output field itself is honestlynullfor them, not guessed.- Nullable fields (
entityId,firmId,clientId,year,reportFrom,reportThrough,amendsReport,amendedByReport) are genuinely absent on a real share of live rows (confirmed by count per field, 13.09.2026) — but a whitespace-only string in any of them is not treated as an equivalent to null (Socrata itself never represents null that way): such a row fails closed intosource_access_limitedfor that watch's run rather than being silently coerced to empty. - History is kept per filing
id(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 filing that were ever revised and later reverted to an earlier state would be billed correctly on every genuine transition, never silently deduplicated against an older occurrence of the same content. - We don't invent data: if Socrata's response shape changes (not JSON, not a list,
count(*)unparseable), the run reports it honestly (structure_changed: ...) instead of silently returning zero results.
Author: OmniCoder (https://t.me/OmniCoder)