Compliance evidence family

Screening Attestation Receipt: settling which list version a counterparty was actually screened against

The Screening Attestation Receipt is a signed statement that a named screening engine, at a named ruleset version bound by a digest, evaluated a keyed commitment to a counterparty identifier against one named primary reference list at a version bound by a digest over that version's contents, plus any supplemental lists at their own versions and digests. The screening instant is placed against a declared external time reference with a declared drift bound, and the verdict is recorded under a recomputed signing authority requirement and a declared validity period. Banks, exchanges, payment firms and their examiners need this.

The property that separates it from a screening log is that the list version cannot be restated after the fact. A log records a version label, and labels get reused when a publisher amends a file in place. Here the receipt carries a digest over that version's contents, and verification requires an independently obtained comparison digest to match exactly. The counterparty is never named: what appears is a keyed commitment under a fixed domain separator, and a gate rejects any plaintext identifier shape in the signed body.

The problem

A firm has to screen counterparties against sanctions and restricted party lists before it does business. Months or years later an examiner, a court, or a counterparty's lawyer asks a question that sounds trivial: what exactly did you check, and against what. The honest answer from most firms is a screenshot and a database row saying a name was screened at a time against a list named by a publisher code and a date. Four things are wrong with that as evidence.

The list version is a label, and labels are not stable. Publishers amend files. A file published under one date can be corrected two days later and reissued under the same date. A firm that screened the earlier bytes has a row saying it screened that version, and so does a firm that screened the later bytes. Nothing distinguishes them. If the correction added the counterparty in question, that difference is the entire case.

The engine configuration is a moving target. Matching thresholds, transliteration rules and fuzzy matching parameters change constantly, usually to reduce alert volume. A clear verdict from a permissive ruleset and one from a strict ruleset are the same word in the log. The time is the firm's own clock, so a screening that appears to predate onboarding is only as good as the clock that wrote the row.

And the evidence itself is toxic to share. The one artifact that would settle the question contains the counterparty's identity, possibly a tax identifier or a wallet address, and possibly the list record that matched. It cannot go to a counterparty, a partner bank or an auditor without creating a data protection problem and, in the match case, tipping off.

Name the parties. The screening firm asserts it screened against the current list with an appropriate ruleset at the right time. The examiner asks for evidence and gets a report the firm produced. A correspondent bank relying on that screening asserts it relied reasonably, based on an assurance it cannot verify. The counterparty, if adverse, asserts it was never properly screened or was wrongly flagged. When screening runs as an autonomous pipeline, no human remembers the state of the system at eleven o'clock on a Tuesday in March. There is only what the system wrote down, and it wrote down a label.

What the receipt binds

The schema is at https://thehiveryiq.com/.well-known/schemas/screening-attestation-v1.json.

FieldMeaning
receipt_typeFixed to screening.attestation.
schemaFixed to r1.0.0.
screening_idIdentifier matching s_ plus up to 63 lowercase alphanumeric, underscore or hyphen.
tenant_idThe owning tenant.
attestation_kindOne of initial, rescreen or supersession.
subject_commitmentThe committed counterparty: scheme, a 64 hex value, a salt_id, a salt_epoch, a key_id, a fixed domain_separator and an identifier_class.
counterparty_classOne of wallet, natural_person, legal_entity, vessel, aircraft or undisclosed.
list_refThe primary list: list_id, list_version, a list_digest, a digest_method, a record_count, an optional publisher_ref and up to 16 supplemental_lists.
screened_atThe UTC instant of screening.
anchorThe time reference: time_source, anchor_time and drift_bound_ms.
engineThe screening system: system_id, ruleset_digest, ruleset_version and engine_key_id.
verdictOne of clear, match, review or indeterminate.
adverse_detailOptional. match_strength_class, matched_list_id and a rationale_digest.
jurisdiction_scopeOne to 32 regime codes, each a two letter country code or EU or GLOBAL.
ttl_secondsThe declared validity period, one second to 2592000 seconds.
countersignrequired_for, required_count, signed_count and up to eight signers, each with key_id, role, sig_b64u and signed_at.
supersession_watchWhether this attestation is being watched for supersession.
supersessionOptional. reason, one to 256 prior_refs, salt_continuity, an optional salt_rotation_ref and scan_completeness.
sequencePosition in an append only ledger: prev_hash and ledger_seq.
evaluated_atThe UTC instant of evaluation.
boundaryThe fixed non attestation string.

The digest_method enum matters. It is sorted_record_merkle_sha256 or sorted_record_concat_sha256, so two parties computing a list digest agree on construction as well as value.

What is deliberately not in the signed body

The counterparty is not named. subject_commitment.value is a 64 character lowercase hex keyed commitment under a scheme of hmac-sha256 or hmac-sha512-256, with a fixed domain_separator of

screening.attestation/subject/v1

That constant is checked by gate, so a commitment computed under a different domain cannot be presented here. The salt is identified by salt_id and salt_epoch and is not in the receipt, so a salt holder can confirm which counterparty this covers and a reader without it cannot enumerate candidates. No list record appears, and no match score. An adverse verdict carries match_strength_class as one of four buckets and a rationale_digest, so the reasoning is committed without being disclosed.

NO_SUBJECT_LEAK enforces the absence. It walks every string outside the exempt digest and signature fields and rejects six shapes: a run of 26 or more hexadecimal characters, a run of 26 or more base fifty eight characters, a nine digit grouped tax identifier form, a passport style alphanumeric run, an electronic mail address form, and a twenty character legal entity identifier form.

Countersignatures use their own domain separated string, with the fixed prefix

hive-countersign screening.attestation

followed by the screening_id, the subject commitment value, the primary list digest, the verdict and the signer's signed_at, joined by single spaces. The implementation documents why it uses those three identifying values rather than the envelope receipt identifier and payload digest: countersignatures live inside countersign.signers, so the payload digest would be a function of the signatures computed over it, and the receipt identifier is minted after they are collected. The verdict is bound explicitly, so a countersignature collected for a review verdict does not verify when moved onto a match verdict.

The receipt envelope uses the shared construction in src/typed/canon.js: canonical JSON with keys sorted recursively, no whitespace, non ASCII escaped as lowercase \uXXXX, lowercase hex SHA-256 over the UTF-8 bytes, and Ed25519 over the ASCII string hive-receipt <receipt_id> <payload_sha256> <ts>.

The gates

Verification runs in this order and stops at the first failure.

  1. SCHEMA. Requires the published schema plus the conditional presence and uniqueness rules the validator subset cannot express, such as a match verdict without adverse detail.
  2. ISSUER_KEY_MATCH. Requires the envelope key to resolve in the trusted keyring, to hold a screening engine role where the registry records roles, and to equal engine.engine_key_id.
  3. PAYLOAD_DIGEST, RECEIPT_ID, SIGNATURE and SIGNER_IDENTITY. The shared envelope layer. The digest of signed_body is recomputed, the identifier has to parse and declare the body's type, the Ed25519 signature has to verify, and SIGNER_IDENTITY fails when only a caller supplied key verified.
  4. ENGINE_IDENTITY_BINDING. Requires engine fields to be well formed and, when an engine registry is supplied, requires ruleset_digest to equal the digest that registry records for ruleset_version.
  5. SUBJECT_COMMITMENT_FORM. Requires a 64 lowercase hexadecimal keyed commitment under a named scheme, the fixed domain separator, a well formed salt identifier and a non negative salt epoch.
  6. NO_SUBJECT_LEAK. Requires that no signed string outside the exempt digest and signature fields carries a plaintext counterparty identifier shape.
  7. LIST_VERSION_BINDING. Requires an independently obtained comparison digest for the named primary list identifier and version, an exact digest match, an agreeing record count, and a recomputable publisher register record when one is named.
  8. LIST_DIGEST_METHOD_AGREEMENT. Requires each declared digest construction to equal the construction the comparison source used for that list identifier and version.
  9. SUPPLEMENTAL_LIST_BINDING. Requires every supplemental list to bind to a comparison digest, to be uniquely named, and never to repeat the primary list identifier.
  10. ANCHOR_DRIFT_BOUND. Requires the anchor reading and the screening instant to lie within drift_bound_ms, unless the time source is declared unanchored.
  11. VERDICT_AUTHORITY_QUORUM. Requires countersign.required_count to equal the count recomputed from the verdict, one for match, one for review and zero otherwise, requires required_for to contain the verdict when that count is positive, and requires every countersignature to verify over the verdict bound signing string.
  12. COUNTERSIGN_DISTINCTNESS. Requires countersigners to be distinct from the engine key, pairwise distinct, held by distinct controllers where the registry reports controllers, and never to sign before the screening instant.
  13. ADVERSE_DETAIL_CONSISTENCY. Requires an adverse verdict to attribute itself to a list actually bound here, and a non adverse verdict to carry no adverse detail.
  14. TTL_FRESHNESS. Requires the reliance instant to fall in the closed interval from screened_at to the conservative expiry, computed as screened_at plus ttl_seconds seconds minus drift_bound_ms milliseconds.
  15. JURISDICTION_SCOPE_COVERAGE. Requires every regime the caller requires to appear in the declared scope, unless the scope declares GLOBAL.
  16. SUPERSESSION_LINKAGE. Requires every named prior attestation to be supplied, to recompute to the declared payload digest, and to agree on commitment, list version and verdict under the declared salt continuity.
  17. BOUNDARY_CONSTANT. Requires the signed boundary text to equal the published constant by exact character comparison.

The boundary

This receipt attests that a named screening engine, at a named ruleset version bound by a ruleset digest, evaluated a keyed commitment to a counterparty identifier against one named primary reference list at a named version bound by a digest over that version's contents, together with any named supplemental lists at their own named versions and digests, at a screening instant placed against a declared external time reference with a declared drift bound, and recorded the stated verdict under a recomputed signing authority requirement together with a declared validity period measured from that screening instant. It does not attest that the reference list itself is complete, accurate, current as published, or free of omission, and it does not attest that the publisher of that list acted correctly. It does not decide whether the counterparty is in fact a sanctioned, restricted or prohibited party, a false negative produced by an honest screening against a correctly bound list version remains a false negative that this receipt does not detect, does not correct and does not disclose, and no gate here detects an engine that reports a clear verdict where its own matching computation produced a match. It does not assess whether the screening program as a whole is adequate, whether the ruleset thresholds are appropriately calibrated, whether the set of lists screened is the correct set for any obligation, or whether the declared validity period is short enough for any purpose. It does not identify the counterparty, does not disclose any list record, does not disclose any match score, and does not confirm that the commitment key or the window salt has remained under the custody of any particular party. It does not authorize, block, freeze, reverse or settle any transaction, does not admit or reject any counterparty, does not constitute a report or notification to any authority, does not constitute legal, regulatory or compliance advice, and whether any obligation is satisfied by the screening it records is determined solely by the applicable law and by the parties' own arrangements.

BOUNDARY_CONSTANT compares that text against the published constant character by character, so it cannot be softened in a verifying receipt.

One clause is unusually blunt: no gate here detects an engine that reports a clear verdict where its own matching computation produced a match. A lying engine is outside the threat model. That is worth writing because it is true, and because it tells a compliance officer where to place the control this receipt does not provide.

An enumerated non attestation beats an implied claim in an examination. An examiner reading a broad assurance has to test the whole assurance. An examiner reading this boundary can see which questions the receipt answers and which remain live. The clause on list completeness works the same way. Reference lists have omissions. A receipt claiming a counterparty is not sanctioned would make a claim about the publisher's work that no screening firm can support, so this one claims something narrower: these exact list bytes were checked, and here is a digest anyone can compare.

Adversarial cases

From test/screening-attestation.test.mjs.

A stale list digest under a current version label fails LIST_VERSION_BINDING. This is the central attack and the central real world error. The label says the current version. The bytes screened were the previous ones. The gate needs an independently obtained comparison digest and compares exactly. A truncated record count fails the same gate, catching a partial file labelled complete. An unsupplied comparison digest source also fails it, so the gate refuses rather than trusting the declaration.

A digest method disagreeing with the comparison source fails LIST_DIGEST_METHOD_AGREEMENT. Same bytes, different construction, different digest. A repeated supplemental list identifier, and a supplemental entry repeating the primary list, both fail SUPPLEMENTAL_LIST_BINDING, so padding the apparent breadth of a screening does not work.

An engine key different from the envelope key fails ISSUER_KEY_MATCH, since the body cannot name one engine while another signs. A ruleset digest absent from the engine registry, and an unknown ruleset version, both fail ENGINE_IDENTITY_BINDING.

A short commitment value, a wrong domain separator, and an uppercase commitment value all fail SUBJECT_COMMITMENT_FORM. Commitments computed under another domain cannot be reused. An address like screening_id and an electronic mail address in tenant_id both fail NO_SUBJECT_LEAK, because free text fields that schema validation accepts are still walked for identifier shapes.

An anchor reading beyond the declared drift bound fails ANCHOR_DRIFT_BOUND. Declaring a tight bound and then reporting an instant outside it is self contradicting.

A match verdict declaring zero required countersignatures fails VERDICT_AUTHORITY_QUORUM. The required count is recomputed from the verdict, so a caller cannot declare its way out of a second signature. A signed count disagreeing with the signer array fails, an unsupplied countersigner registry fails, and a countersignature moved from a review verdict fails because the verdict is inside the countersign signing string.

Two key identifiers under one controller fails COUNTERSIGN_DISTINCTNESS. One person holding two keys is one person. A countersignature predating the screening fails the same gate. Adverse detail naming an unbound list fails ADVERSE_DETAIL_CONSISTENCY.

Reliance after the conservative expiry fails TTL_FRESHNESS, and reliance before the screening fails too. Reliance exactly at the expiry passes, which is tested explicitly. With no reliance instant supplied, the verifier substitutes the local clock and marks the result clock dependent rather than pretending it was anchored. Scope missing a required regime fails JURISDICTION_SCOPE_COVERAGE.

An unsupplied prior attestation, a prior commitment mismatch, a prior payload digest mismatch, and a list_version_update naming the same version all fail SUPERSESSION_LINKAGE. A rescreening chain has to chain.

Altered boundary text fails BOUNDARY_CONSTANT, and a post signature tamper fails the payload digest gate. Five positive cases confirm the shape: a clean clear verdict, a countersigned match verdict, same salt and rotated salt supersession records, and an indeterminate verdict that correctly requires no countersignature.

Performance

From hive-verifier-api/benchmarks/results-latest.json, measured at 2026-08-09T22:28:01.099Z at commit e167719f7d815809c6f55c5dde4495edf1c5615f over 200 iterations:

MeasureValue
Mint p505.748 ms
Mint p956.257 ms
Verify p503.908 ms
Verify p957.281 ms
Artifact size4237 bytes

Local harness measurement on one machine and one Node version, recorded on Node v20.20.1, linux/x64, two Intel Xeon cores at 2.90 GHz. It is a reproducible measurement of this code, not a production latency guarantee. This is among the largest artifacts of the deployed types in this run, which follows from a body carrying supplemental lists, countersigners and supersession refs.

How to run it

  • Host: https://thehiveryiq.com/v1
  • Verify route: /verify/screening-attestation, no credentials required
  • Mint route: /mint/screening/attestation, requires Authorization: Bearer $HIVE_TYPED_MINT_TOKEN
curl -sS -X POST https://thehiveryiq.com/v1/verify/screening-attestation -H 'content-type: application/json' -d '{"receipt": <a receipt of this type>}'

Mint fails closed with 401 when the token is missing.

Where it sits in the canon

Family: compliance evidence. Category: typed receipt contract.

It chains to itself through supersession and sequence, so rescreenings over one commitment form a verifiable history rather than a pile of rows. It refers outward to a publisher register record through publisher_ref, which is where the list version binding gets its anchor. It does not replace a screening engine, a sanctions program, or a filing.

Status

production_deployed.

References for this instrument
Canon entry
Screening Attestation Receipt in the Hive canon
JSON Schema
https://thehiveryiq.com/.well-known/schemas/screening-attestation-v1.json
Verify route
POST /verify/screening-attestation
Canonical type
screening.attestation
Private by design. Hive does not store your prompts. Every request is already receipted by a one-way SHA-256 fingerprint, not the words. Proof, not surveillance.