Case file
Blockchain anchored primary source verification for healthcare credentials. Not a database and not a decentralisation story: a timestamp that no party to the dispute controls.
Product thesis
Credentialing a physician at a new hospital takes about 30 days, and almost none of that is work. It is queue time: waiting for a state board office to answer a faxed verification request, waiting for a credentialing coordinator to reach the top of a stack, waiting for a primary source letter to arrive in the post. The bottleneck persists because the verification has to come from the issuing board rather than from a copy, the boards answer at their own pace, and no hospital can accept another hospital's word for it. Every institution repeats the same check on the same physician, and each repetition costs a month of a clinician's earning time and a coordinator's salary. The second problem is that the result decays and cannot be proven later. A licence verified in March says nothing about April, and the credentialing file recording the March check is a database row that anyone with access can edit. When a negligent credentialing claim arrives, the hospital's defence rests on a record whose integrity it cannot demonstrate. Blockchain anchoring is the right instrument for exactly that second problem and nothing else: it is not a database, not a payment rail, and not a decentralisation story. It is a timestamp that no party to the dispute controls. Writing the hash of a credential state to a public chain converts "we checked this on the fourteenth" from an assertion into an artefact, and it does so without putting a single identifying fact on a public ledger.
Architecture
Three services. One writes credential facts, one proves them, one presents them.
Crawls the licensing board of every state in the programme, plus the National Practitioner Data Bank, on a 24 hour cycle. It reads the original register, not a copy, because that is what primary source verification means and it is the foundation everything else rests on.
It runs as an AWS Lambda container image on an EventBridge schedule, with a fifteen minute flexible start window so fifty board portals do not all see Credool arrive at the same second. The same code runs as a FastAPI service for manual cycles and local development.
Collects verified credential states, builds a Merkle tree, and writes the root to Polygon. It stores the proof for every leaf and serves it on request. The anchoring contract has no update path and no delete path.
It also runs a background sweep independent of any message from the Loop, so anchoring depends on stored state rather than on a notification having been delivered.
A React Native passport app plus an API that issues short-lived verification codes. A clinician generates a QR code, a coordinator at a ward entrance scans it with a phone camera, and a web page returns the verdict, the credential summary behind it, and the chain anchors that make the summary checkable.
The coordinator needs no Credool account. Requiring one would push the whole flow back to email and phone calls, which is the process this replaces.
Key engineering decisions
Polygon over Ethereum mainnet. Anchoring is a write-heavy, value-light workload. A batch anchor costs roughly 70,000 gas. On mainnet that is enough per batch that the system would come under constant pressure to batch less often, which directly lengthens the window between a credential changing and that change being provable. On Polygon the same write costs a fraction of a cent, so batches seal hourly and nobody weighs gas against freshness. Polygon settles to Ethereum, so the security argument does not stop at Polygon validators.
Merkle batching over per-record anchoring. One transaction per credential is simpler to explain and about five hundred times more expensive at a batch size of 512. Batching buys that back at the cost of one extra step in verification: the verifier checks a proof instead of doing a lookup. That step is cheap and standard, so the trade is obvious. The real cost is that a proof becomes an object the system must store and serve correctly, which is why every proof is verified against its root before the batch leaves the sealing function. A bad proof found at scan time is a support call during a shift handover. Found at seal time it is a log line.
The hashing follows the OpenZeppelin StandardMerkleTree convention exactly, rather than an in-house scheme. CI generates vectors with the reference library for trees of 1, 2, 3, 5, 8, 13, and 64 leaves and checks the Python builder against every root, leaf hash, and proof path. Following a published convention means an auditor can rebuild any batch with a public npm package and never has to trust a line of Credool's code. An in-house scheme would leave Credool as the authority on whether its own proofs are valid, which is the position anchoring exists to escape.
An explicit crawl failure taxonomy over silent staleness. This is the decision the rest of the system is organised around. The most damaging failure in credential monitoring is not a crash, it is a crawler that swallows a timeout and leaves a green ACTIVE badge on screen that was last confirmed six weeks ago. A hospital reading that badge believes it is looking at a fresh check.
So the Loop never returns a boolean. Every attempt ends in one of eight explicit outcomes, and each one separately answers three questions that most crawlers conflate: may this overwrite the stored credential, is a retry likely to help, and does a human need to look at it. Only three outcomes may write credential state. A 502 from the Texas Medical Board preserves the last confirmed status and marks it stale. A parsed page saying REVOKED overwrites it and escalates. Those are not the same event.
Three consequences fall out of the taxonomy that would not have been obvious without it. An unrecognised exception escaping a crawler is classified as a parse error, not a transport error, because an unknown crash is a defect in our code and defects must be loud rather than retried quietly for a week. An unmapped licence status phrase raises rather than defaulting, because inventing ACTIVE for a string nobody has seen is how a suspended physician gets a green badge. And New York, where licensure and discipline live at two different agencies, refuses to write a half answer: if the discipline source is unreachable, the credential keeps its previous state rather than being recorded as active with an empty disciplinary history, which would be a false statement dressed as a verified one.
A fifteen minute QR TTL as the security boundary. The code is not a JWT. A JWT carries its own claims, so a photograph of the code discloses a clinician's credential state and stays readable after revocation. The Credool payload is an opaque handle plus an HMAC signature, and it discloses nothing on its own.
Expiry is enforced by Redis key expiry rather than by a timestamp inside the payload. A timestamp in the payload is a claim the holder presents. A key that no longer exists is a fact the server observes. Fifteen minutes is long enough to walk from a car park to a desk and short enough that a screenshot forwarded to a colleague stops working before the end of a shift handover. At most three codes are live per clinician, so a lost phone is a bounded problem rather than a trail of working codes.
What the anchor badge means to a compliance officer
The BlockchainAnchorBadge shows a Polygon transaction hash, a network indicator, an anchor timestamp, and a block number. It is not decoration and it is not a trust seal. It is a pointer to a public record that a hospital's own engineer can open in a block explorer without asking Credool for anything.
The claim it makes is narrow and therefore defensible. It does not say the physician is good. It says: at this block, at this timestamp set by a network that Credool does not control, a root existed that commits to this exact credential state. Combined with the Merkle proof served alongside it, a third party can verify that the credential record they are holding is the one that was committed, and that it has not been altered since.
That is the legal argument. In a negligent credentialing claim the question is not usually whether a hospital checked, it is whether the hospital can prove what it knew and when. A credentialing file proves neither, because it has no independent timestamp and no integrity guarantee. Discovery turns into an argument about database access logs and backup retention. An anchored root removes the argument. It is also symmetric, which is worth stating: a clinician whose licence was clean on the day they were cleared has evidence of that fact that does not depend on the hospital's cooperation.
The complementary contract matters as much. MerkleCredential answers whether a credential state was ever anchored. VerificationRegistry answers the question an auditor actually asks: prove your agency checked this physician before the shift on the fourteenth. Those are different claims, so they are different contracts with different write paths, and the registry verifies the Merkle proof on chain before it records anything. A registry that accepted unproven claims would record assertions, not verifications.
What is on chain is one 32 byte root per batch and nothing else. No name, no NPI, no licence number, no date of birth. A leaf commits to a credential id, a content hash, and an observation time, and is meaningful only to someone who already holds the record it describes. That is what makes public anchoring compatible with a HIPAA posture rather than in tension with it, and it is the first question a chief compliance officer asks.
Public and private split
The smart contracts are public, and there was never a decision to make. Bytecode on a public chain is readable by anyone, source verification on Polygonscan is expected, and the security argument for the anchoring model depends on a hospital being able to read the contract rather than trust a description of it. Keeping the source closed would buy nothing and cost the credibility the design rests on.
The same reasoning extends to the crawler base class, the failure taxonomy, the HTTP transport layer, the FHIR integration, and the QR protocol. These are the parts a buyer needs to audit before trusting the system, and the parts whose correctness argument is stronger when it can be checked.
What stays private is the crawling method for specific board portals. Forty seven of the fifty adapters live in a package loaded through an environment variable, along with the NPDB Continuous Query enrollment and session workflow. This is not a business model dressed up as a technical boundary. The selectors, rate tolerances, session handling, and per-portal quirks for a given state board are the output of field work against a system with no documentation and no API, and they are the thing that is genuinely hard to reproduce. Three reference adapters ship publicly, chosen because each demonstrates a different failure shape the taxonomy has to survive: California for rate limiting on a shared host, New York for the two-agency partial answer, Texas for a maintenance window that mimics an empty result set.
The registry loads the private pack when present, logs and continues when it is absent, and raises when it is present but broken. Starting a cycle believing 47 boards are covered when they cannot load is precisely the silent staleness the taxonomy exists to prevent.
Build status
The failure taxonomy, with the write rule enforced in the repository layer rather than left to callers, retry and backoff policy, and per-board circuit breaker. 77 tests.
The Merkle builder, verified against OpenZeppelin reference vectors in CI. 30 tests.
Both smart contracts, with access control, single-write anchoring, on-chain proof verification, and custom errors. 14 tests.
QR issuance, TTL enforcement, and verification, including the signature scheme, the active-code ceiling, revocation, and the credential-state gate on issuance. 24 tests.
The SMART on FHIR client: discovery with CapabilityStatement fallback, PKCE, constant-time state comparison, and refresh token rotation handling. Real, not mocked.
The HR dashboard, which builds and typechecks clean and renders the full credential detail screen, coverage map, cycle panel, and verification page.
The contracts compile and their tests pass against a local EVM, and the deploy
script has been run end to end against a local Hardhat network. They have not
been deployed to Polygon Amoy, because that requires a funded key from a
faucet request against an address the operator controls. There is no transaction
hash to publish here, and fabricating one would defeat the purpose of a document
whose subject is verifiable evidence. docs/handover.md carries the exact
sequence, and the deploy script writes the addresses, hashes, and block numbers
to a JSON file on completion.
Mumbai, named in the original specification, was retired by Polygon in April 2024. Amoy replaced it and is what the configuration targets.
NPDB checks need Continuous Query credentials, which is a subscription requiring a registered entity and per-subject enrollment, not an API key obtainable in an afternoon. The public schema, the report classification mapping, and the credential gate are implemented. Without credentials the adapter runs and returns RECORD_UNAVAILABLE for every subject rather than being disabled, so a hospital can see that the federal check was attempted and could not answer. Absence of a report and inability to ask are different facts and the passport distinguishes them.
The 47 private board adapters need the pack installed. Anchoring needs a signing
key and deployed contract addresses; without them the ledger builds trees and
serves proofs but reports can_submit: false rather than pretending a batch was
written.
The mobile app is written and typechecks clean but has not been run on a
device. The
Terraform passes terraform validate but has never been applied to a real
account. The three reference board adapters were written against checked-in
fixtures rather than live traffic, and refreshing those fixtures is the
documented first step of any rollout.
Database migrations and multi-tenant scoping. The data model currently assumes one operator.
Every capability above degrades visibly. None of them fails quietly. That is not a feature list, it is the single property the system is organised around, and it is the one a hospital chief compliance officer should test first.