cf-exclusion-check
Runs the exclusion-screening service on Cloudflare's free-tier infrastructure: Cloudflare Workers execute the request handlers, cron triggers merge monthly LEIE supplement rows and run HEAD freshness probes, Cloudflare D1 stores the 247,583 exclusion records queried per request, and Cloudflare's rate-limiting/row-read limits shape the service.
Deploys the MCP server and its /v1/check, /v1/report, /v1/health, /.well-known/x402 and /mcp endpoints as a stateless Cloudflare Worker, including scheduled cron jobs for supplement ingestion and freshness checks against the exclusion data.
Handles pay-per-call monetization through the Coinbase CDP facilitator: unpaid requests return a 402 with an x402 v2 payment challenge (USDC on Base mainnet, eip155:8453), settlement is after-handler, and the service is listed/discovered via CDP's Bazaar with EIP-3009 gas sponsorship.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cf-exclusion-checkCheck if John Smith is on the OIG exclusion list."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
CF Exclusion Check
Pay-per-call screening against the HHS-OIG List of Excluded Individuals and Entities (LEIE) and the SAM.gov exclusions list, on Cloudflare Workers. Charged per request with x402, and exposed to agents as an MCP tool.
Every result states why it matched. A screening answer that cannot be explained is worse than no answer, because someone may act on it.
Live: https://cf-exclusion-check.cf-exclusion-check.workers.dev
Network: Base mainnet (eip155:8453) via the Coinbase CDP facilitator.
Payments are real. Settlement is after-handler, so a request that cannot be
served is not charged.
What it screens
source | rows | what it is |
| 83,975 | the LEIE active exclusions list |
| 1,061 | 20 months of LEIE reinstatement supplements |
| 162,547 | the SAM.gov government-wide exclusions list |
total | 247,583 |
SAM.gov is government-wide rather than healthcare-specific, and it is not mostly businesses: 79.1% of its records are individuals, 15.2% are "Special Entity Designation", 4.9% firms and 0.8% vessels. The largest excluding agencies are HHS (69,978), OFAC (41,712) and OPM (40,595).
The reinstatement supplements matter. UPDATED.csv holds only currently
excluded subjects, so a reinstated person simply disappears from it and a bare
"no match" hides their history. With the supplements loaded, the same query
answers the more useful thing:
matched — excluded 2020-01-20, reinstated 2025-01-22, not currently excluded
What it does not screen
state Medicaid exclusion lists
licensure board actions
A match here is a name match, not an identity determination (unless it was made on an identifier), and a non-match is not a clearance. Confirm against the official record at https://exclusions.oig.hhs.gov (LEIE) or https://sam.gov/search (SAM) before acting against a person or business.
Related MCP server: oig-leie-mcp
Endpoints
endpoint | price | notes |
| $0.05 | verdict, confidence, basis, reason per match |
| $0.50 | every match with full source fields |
| free | row counts, load dates, freshness |
| free | discovery document (also at |
| $0.05 per paid tool call | JSON-RPC; discovery is free |
| — |
|
Query fields
field | notes |
|
|
| 10 digits. A strong identifier. |
| SAM Unique Entity Identifier, exactly 12 alphanumeric chars. Strong. |
| CAGE code, exactly 5 alphanumeric chars. Strong, but on only 0.3% of SAM rows. |
|
|
| Two letters. Corroborates only — never creates or suppresses a match. |
Verdicts
verdict | meaning |
| matched on an identifying basis and the record carries no reinstatement date |
| matched only on surname + first initial. A lead, not a finding. |
| matched, but every match has been reinstated |
| nothing matched |
excluded requires an identifying basis — an NPI, UEI or CAGE, a full
personal name, or a business name. A surname plus a first initial never produces it, even when a
supplied state agrees. Sharing a state with millions of people is
corroboration, not identification, and "excluded" is a verdict someone can lose
a job over.
Each match also reports basis: npi, uei, cage, business_name,
full_name or surname_initial.
Is the exclusion still in force?
The two sources say "no longer excluded" differently, so neither answer is derived from the other. LEIE carries a reinstatement date — present means reinstated. SAM carries a record status and a termination date, the date the exclusion is scheduled to end; it is usually absent (indefinite) or far in the future, with year 2227 used as an indefinite placeholder. Of 168,452 SAM records only 12 have a termination date in the past. Anything not positively known to have ended is treated as still in force.
Two counts, deliberately
match_count is matching rows; subject_count is distinct subjects.
LEIE records some subjects more than once — one person in the list has three
rows differing only by address — so reporting a single number would read as
three separate exclusions.
Paying
An unpaid call returns 402 with the machine-readable challenge in the
payment-required header (base64 JSON, x402 v2):
{"x402Version":2,"accepts":[{"scheme":"exact","network":"eip155:8453",
"amount":"50000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo":"0xCa28eb92657F8a81aFb5493b3a04A740B204d816","maxTimeoutSeconds":60,
"extra":{"name":"USD Coin","version":"2","paymentFlow":"authorization"}}]}amount is atomic USDC (6 decimals): 50000 = $0.05, 500000 = $0.50.
node scripts/x402-client.mjs # /v1/check
node scripts/x402-client.mjs /v1/report "Some Name"Gas is paid by the facilitator under EIP-3009, so a payer needs USDC only.
Staying listed in the Bazaar (30-day keepalive)
/v1/check has been listed in CDP's Bazaar since a settlement on 2026-09-11.
The Bazaar removes a route that goes 30 days without a settled payment, and
eventually drops one whose bare URL stops answering 402 Payment Required. So:
Settle at least one paid call per listed route inside every 30-day window; run
./scripts/verify-mainnet.sh(one $0.05/v1/check) at least every three weeks./v1/reportis also listed as of a settlement on 2026-09-11 — keep both routes alive, not just/v1/check.Check the listing without paying:
./scripts/verify-mainnet.sh --bazaar-only
curl -s "https://api.cdp.coinbase.com/platform/v2/x402/discovery/merchant?payTo=0xCa28eb92657F8a81aFb5493b3a04A740B204d816"The payout address is shared with booth #2 (cf-package-check), so the merchant
lookup lists both booths' routes; --bazaar-only counts only this host.
MCP
curl -X POST $BASE/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'tool | price |
| $0.05 per call |
| free |
initialize, ping and tools/list are free so an agent can discover the tool
and its price before paying — charging for discovery would make it
undiscoverable. Only tools/call on the paid tool is charged.
Implemented as stateless JSON-RPC rather than with McpAgent, which keeps
session state in a Durable Object; every method used here is a pure
request/response exchange, so no session state is needed and the service stays
on the free tier.
Rate limits
bucket | limit |
free | 10 / min |
paid (request carries a payment header) | 60 / min |
Keyed on a salted hash of the client IP. /v1/health and the discovery document
are never limited — they are how a caller checks whether the service is
alive, and throttling them would make an outage indistinguishable from a limit.
Architecture, and one honest constraint
The loader is split, because the full list cannot be loaded inside a Worker on the free plan:
oig.hhs.govignoresRangerequests — a byte-range request returns HTTP 200 with all 15,608,468 bytes — so the file cannot be walked in slices.The free plan gives 10 ms CPU per invocation, cron triggers included. Parsing 15.6 MB is far past that.
work | where |
LEIE bulk generation (83,975 rows) |
|
SAM bulk generation (162,547 rows) |
|
monthly LEIE supplements (41–200 rows) | Worker cron |
freshness of the bulk generation | Worker cron, |
SAM has the same problem for a different reason: its Exclusions API caps size
at 10 records per page, so 168,452 records would need ~16,846 requests
against a published limit of 10–1,000 requests per day. Paging is not slow, it is
impossible. The asynchronous extract returns the whole set in one download.
Supplements add new exclusions and record reinstatements. They do not catch
a record removed for any other reason — only a full reconcile does, and that
needs a reseed. So /v1/health reports freshness.reseed_due from a daily HEAD
probe rather than implying the data is current.
docs/SOURCES.md records every measurement behind these claims.
Free-tier caveats
This runs on the Cloudflare free plan deliberately. Four limits shape it, and two of them bite in ways the documentation does not prepare you for.
limit | documented | what actually happened |
D1 rows read | 5,000,000 / day | exceeded, then refused inconsistently across query shapes — a one-row probe can succeed while a larger query fails |
D1 rows written | 100,000 / day | reached 1,698,310 before writes were refused |
D1 database size | 500 MB | currently 110 MB at 247,583 rows |
Worker CPU | 10 ms / invocation, cron included | hard; it is why bulk loading cannot run in-Worker |
Bulk reloads are local-only, by design. Every bulk generation is built on a
workstation with scripts/build-*-seed.mjs and applied with
wrangler d1 execute. Nothing reloads a full source from inside the Worker. The
cron only merges the small monthly LEIE supplements and runs a HEAD freshness
probe.
Do not design against either number as if it were hard. The documented ceilings are not enforced strictly, and the observed slack is not a guarantee. Two consequences seen in practice:
A single full-table scan per request (one missing index) burned the daily read budget after roughly 59 paid calls, and surfaced as a
500on a request whose payment had already been authorised — the caller pays and gets nothing. Hence the index discipline inmigrations/0004and0005, and the query-plan assertions intest/sql.test.ts.When the write budget ran out, request-log inserts began failing. They had been wrapped in an empty
catch, so settled payments silently went unlogged. The write is now awaited and failures are surfaced.
A bulk reload spends roughly 5.15 row writes per row (one table write plus ~4.15 partial-index entries), so reloading SAM costs ~837,000 writes. Schedule reloads accordingly, and expect the audit log to degrade while one is running.
Never count rows at request time. The sharpest lesson here: a single
SELECT source, COUNT(*) FROM exclusions GROUP BY source in the provenance
lookup ran on every screening request and every health check. Its plan is
SCAN ... USING COVERING INDEX — a pass over all 247,583 rows — so each call
spent ~247k of the daily read budget and capped the service near 20 requests a
day. Row counts now come from source_load_progress, which already tracks
them. test/sql.test.ts asserts that neither the provenance lookup nor
/v1/health references the exclusions table, because this is an easy mistake
to reintroduce and its cost is invisible until the budget is gone.
Local development
npm install
npm test # 71 tests, 17 of them executing real SQL
npm run typecheck
npm run migrate:local
npm run seed:build # downloads UPDATED.csv, emits seed/ parts
node scripts/build-rein-seed.mjs
node scripts/build-sam-seed.mjs # needs .secrets/sam-exclusions.raw
npx wrangler d1 execute cf_exclusions --local --file=seed/leie-seed.00.sql # …and each part in order
printf 'ADMIN_TOKEN=dev\nIP_HASH_SALT=dev\n' > .dev.vars
npx wrangler devApply seed parts in order with a retry loop: a single 24 MB file makes
wrangler d1 execute --remote fail in its post-upload ingest poll, and
individual parts fail transiently and succeed on retry. wrangler d1 import
does not exist in wrangler 4.131.0, and migrations apply --remote needs
CI=true to run unattended.
Verifying mainnet end to end
./scripts/verify-mainnet.sh # one attempt; exits 2 if not ready
./scripts/verify-mainnet.sh --wait # poll until ready, then verifyIt makes one real paid call, then checks the on-chain receipt at PAY_TO, the
paid=1 audit row and its transaction reference, and whether the Bazaar has
indexed the resource.
It will not spend money unless the service is demonstrably serving. The gate
is four consecutive full health checks plus one more immediately before paying,
because under D1 quota pressure the service answers intermittently and every
cheaper readiness proxy tried here reported ready while a screening still
failed: SELECT 1 succeeds at zero rows read, a one-row read succeeds while a
larger query fails, and a single /v1/health 200 was followed immediately by a
503. A run during an outage costs nothing and reports NOT READY.
Deploying
npx wrangler secret put ADMIN_TOKEN
npx wrangler secret put IP_HASH_SALT
npx wrangler deployGoing to mainnet
See docs/MAINNET.md for the verified procedure. In short,
it is not a two-line change: the configured facilitator
(https://x402.org/facilitator) is testnet-only — its /supported endpoint
does not list eip155:8453 — so mainnet also requires Coinbase CDP facilitator
credentials. Flipping NETWORK alone makes every paid request fail.
The Base mainnet USDC contract is already mapped and verified on-chain
(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, chainId 8453, 6 decimals).
PAY_TO is a real mainnet receiving address, not a build-script key — see
docs/MAINNET.md for how and why it was rotated. Payments
settle in real USDC. The testnet-only key that scripts/gen-testnet-key.mjs
mints is still used for .secrets/testnet-payer.json, which spends on
testnet; it was never the receiving address and should not be confused with
PAY_TO.
Privacy
Raw client IPs are never stored. They are HMAC'd with
IP_HASH_SALT; without a salt no hash is written at all, because an unsalted SHA-256 of an IPv4 address is trivially reversible across the whole 2^32 space.Queried names are never stored, only a salted hash, so the log cannot become a list of who was screened.
Street addresses are in the source data but are never selected and never returned by any endpoint.
Licence
MIT. LEIE data is published by the HHS Office of Inspector General and is in the public domain; this project is not affiliated with or endorsed by HHS-OIG.
This server cannot be deployed
Maintenance
Related MCP Connectors
Pay-per-call screening against HHS-OIG LEIE and SAM.gov exclusions.
31Pay-per-call compliance lead scanners with free MCP discovery and x402 payments.
Pay-per-call compliance via x402: wallet sanctions, OFAC name, UK + US company verification.
Screen people & companies against 12 US sanctions & exclusion lists (OFAC, LEIE, SAM, Medicaid).
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables MCP-compatible agents to discover and call x402 paid services from a directory of over 2,000 APIs.Apache 2.0
- AlicenseNot gradedqualityCmaintenanceScreens individuals and entities against the OIG LEIE exclusion list, returning candidate matches that require human verification.MIT
- FlicenseNot gradedqualityAmaintenanceEnables agents to discover and verify paid agent services through x402 payment validation and release gate checks.-
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to perform medical-billing tasks like prior authorization checks, claim denial decoding, unpaid claim triage, appeal assessments, and PHI de-identification using deterministic rules engines with per-call payments via the x402 protocol.18MIT