Skip to main content
Glama
cfgenius145-sketch

cf-exclusion-check

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

leie

83,975

the LEIE active exclusions list

leie_rein

1,061

20 months of LEIE reinstatement supplements

sam

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

POST|GET /v1/check

$0.05

verdict, confidence, basis, reason per match

POST|GET /v1/report

$0.50

every match with full source fields

GET /v1/health

free

row counts, load dates, freshness

GET /.well-known/x402

free

discovery document (also at …/x402.json)

POST /mcp

$0.05 per paid tool call

JSON-RPC; discovery is free

POST /admin/reload

ADMIN_TOKEN required

Query fields

field

notes

name

"John Smith", "Smith, John" or a business name. Wildcards rejected.

npi

10 digits. A strong identifier.

uei

SAM Unique Entity Identifier, exactly 12 alphanumeric chars. Strong.

cage

CAGE code, exactly 5 alphanumeric chars. Strong, but on only 0.3% of SAM rows.

dob

YYYYMMDD.

state

Two letters. Corroborates only — never creates or suppresses a match.

Verdicts

verdict

meaning

excluded

matched on an identifying basis and the record carries no reinstatement date

possible_match

matched only on surname + first initial. A lead, not a finding.

reinstated_only

matched, but every match has been reinstated

no_match

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/report is 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

exclusion_check

$0.05 per call

exclusion_sources

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.gov ignores Range requests — 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)

scripts/build-seed.mjs + wrangler d1 execute

SAM bulk generation (162,547 rows)

scripts/build-sam-seed.mjs + wrangler d1 execute

monthly LEIE supplements (41–200 rows)

Worker cron

freshness of the bulk generation

Worker cron, HEAD only

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 500 on a request whose payment had already been authorised — the caller pays and gets nothing. Hence the index discipline in migrations/0004 and 0005, and the query-plan assertions in test/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 dev

Apply 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 verify

It 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 deploy

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

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers