What you can request
A request is a DCQL query, which names the credential and the claims to disclose, plus optional predicates, an optional nullifier and a purpose. The sandbox verifier accepts only requests on its allowlist, so a requester cannot ask for more than the sandbox permits. This page describes the sandbox police-certificate credential and what the allowlist lets you ask for.
The code on this page uses the verifier SDK. Requesting and reading proofs covers installing it and calling it from a Next.js app.
The sandbox police-certificate credential
This section is the synthetic sandbox police-certificate attestation:
| Property | Value |
|---|---|
| Schema | sandbox.tridentgold.pcoc-attestation, version 1, schema uid 4096 |
| Format and suite | zknc, ZKNC-1 |
| Issuer | The sandbox issuer, issuer id 17 |
It is a synthetic sandbox credential. It describes no real person and is not an authoritative Police Certificate of Character.
It has four claims:
| Claim | Type | What it holds | Disclose | Prove |
|---|---|---|---|---|
record_status | enum | The outcome of the synthetic record check. In this schema clear is the only value it can take. | Yes | Equals clear (equality or enum), or is in a set of values (enum-set) |
decision_date | date | The date of the synthetic decision. | No | Falls within a date range (range). The range must span at least 30 days. |
reference_no | string | A synthetic reference number. | No | Nothing |
subject_ref | reference | A reference to the synthetic subject record. | No | Nothing |
reference_no and subject_ref cannot be requested at all: they cannot be
disclosed and no predicate may use them. decision_date cannot be disclosed
or compared for equality; only a range at least 30 days wide is allowed, so a
requester cannot narrow the date down to a single day.
Build a request with the SDK
policeCertificate() builds a request for this credential:
import { policeCertificate } from "@human.tech/tridentgold-zknc-mvp-verifier";
const input = policeCertificate({
purpose: "Confirm a clear record for volunteer screening",
disclose: ["record_status"],
decidedWithin: { days: 180 },
nullifier: { scopeLabel: "volunteer-screening" }
});
| Option | What it asks for | Where it appears in a verified result |
|---|---|---|
purpose | Required. Shown to the person on the consent screen. One line of plain text, at most 140 characters. | |
disclose: ["record_status"] | Disclose record_status. | result.disclosed.record_status |
recordStatus: "clear" | Prove record_status is clear without disclosing it. | result.predicates.record_status_clear |
decidedWithin: { days } | Prove decision_date is within the last days days, up to and including today (UTC). days must be at least 30. | result.predicates.decision_date_in_range |
decidedBetween: { from, to } | Prove decision_date is between two ISO dates (YYYY-MM-DD), inclusive. Use instead of decidedWithin. | result.predicates.decision_date_in_range |
nullifier: { scopeLabel } | Ask for a nullifier under this scope label. See Nullifiers. | result.nullifier |
The builder returns plain data in the raw form below, so you can log it, inspect it or extend it.
Write the same request as raw DCQL
requestProof() also accepts a DCQL query and predicates directly. This is the
same request as the builder call above:
const DAY_MS = 86_400_000;
const today = new Date().toISOString().slice(0, 10);
const from = new Date(Date.now() - 180 * DAY_MS).toISOString().slice(0, 10);
const input = {
purpose: "Confirm a clear record for volunteer screening",
dcqlQuery: {
credentials: [
{
id: "pcoc",
format: "zknc",
claims: [{ id: "record_status", path: ["record_status"] }]
}
]
},
predicates: [
{
id: "decision_date_in_range",
path: ["decision_date"],
op: "range" as const,
lo: from,
hi: today
}
],
nullifier: { scopeLabel: "volunteer-screening" }
};
dcqlQueryis a standard DCQL query and is sent as written. In this sandbox every DCQLclaimsentry is a disclosure.metamay be omitted, as here: the verifier fills in the sandbox schema, suite and issuer. If you include it, it must match them.predicateseach have anidyou choose, apathnaming one claim, and anop:equalityorenumwithvalue,enum-setwithcodes, orrangewithloandhi(inclusive). Dates areYYYY-MM-DD; enum values are their names, such asclear.- A verified result reports each predicate under the
idyou chose. - Credential, claim and predicate
ids are 1 to 64 characters of letters, digits,_and-. The verifier answers any other id withrequest_invalid. - A predicate missing an operand its
oprequires (valueforequalityandenum,loorhiforrange,codesforenum-set), or carrying one itsopdoes not use, is also answered withrequest_invalid, with norule. Operands that are present but outside the allowlist get the rules below.
See what was sent
The request that requestProof() returns carries, in request.request, the
parts of the signed request you control, as signed. Its dcqlQuery is the
DCQL query the wallet received, including the meta the verifier filled in:
{
"purpose": "Confirm a clear record for volunteer screening",
"dcqlQuery": {
"credentials": [
{
"id": "pcoc",
"format": "zknc",
"meta": {
"suite": "ZKNC-1",
"schemas": [
{ "id": "sandbox.tridentgold.pcoc-attestation", "uid": "4096", "version": 1 }
],
"issuer_ids": ["17"]
},
"claims": [{ "id": "record_status", "path": ["record_status"] }]
}
]
},
"predicates": [
{
"id": "decision_date_in_range",
"path": ["decision_date"],
"op": "range",
"lo": "2026-03-27",
"hi": "2026-09-23"
}
],
"nullifier": { "scope": "trident-gold-integrator-demo.vercel.app/volunteer-screening" }
}
The DCQL query keeps its standard member names, such as issuer_ids; the
fields around it use the SDK's camelCase names.
Nullifiers
A nullifier is a value derived from the holder and a scope. The same holder answering requests with the same scope label gives the same nullifier each time; a different scope label gives an unrelated value. It reveals nothing else about the holder. Use it to recognize a returning holder, for example to notice that the same holder has already registered for a programme.
Ask for one with nullifier: { scopeLabel }. The label is lowercase letters,
digits, _ and -, starting with a letter or digit, at most 63 characters.
The verifier prefixes its namespace, so the full scope is
trident-gold-integrator-demo.vercel.app/<scopeLabel>, echoed as
request.request.nullifier.scope. A verified result then carries the value in
result.nullifier.
The allowlist
Beyond the per-claim rules above, a request must:
- ask for exactly one credential, the sandbox police-certificate attestation,
and not use DCQL
credential_setsor DCQLvaluesmatching; - disclose at most 3 claims;
- use at most 4 equality predicates, and at most one per claim;
- use at most 1 range predicate, and at most 1
enum-setpredicate with at most 8 values; - carry a purpose: one line of plain visible text, at most 140 characters, with no leading or trailing spaces or control characters.
The allowlist is machine-readable. getPolicy() returns it (verifier is the
client from Create the client):
const policy = await verifier.getPolicy();
policy.claims.decision_date; // { type: "date", disclose: false, ops: ["range"], minRangeDays: 30 }
When requestProof() can fetch this policy and it is policy_version 1, the
SDK checks the request against it before sending. Otherwise it sends the
request unchecked. The verifier always checks it. A request outside the allowlist throws
ZkncRequestNotPermittedError, whose rule names the rule that failed:
| Rule | Meaning |
|---|---|
credential.count | The query does not ask for exactly one credential. |
credential.format, credential.schema, credential.issuer | The credential's format, suite, schema or issuer is not the sandbox's. Omit meta to use the sandbox's. |
credential_sets.forbidden | DCQL credential_sets are not supported. |
claim.unknown, claim.path_invalid | A path does not name exactly one known claim, or a claim is disclosed twice. |
claim.values_forbidden | DCQL values matching was used, or an equality or enum predicate's value is one the claim cannot take. |
<claim>.disclose_forbidden | That claim cannot be disclosed, for example decision_date.disclose_forbidden. |
<claim>.op_forbidden | That predicate operation is not allowed on that claim. |
predicate.id_duplicate, predicate.op_unknown | Two predicates share an id, or an op is not one of the four. |
range.count | More than one range predicate. |
range.bounds_invalid | A bound is not an ISO YYYY-MM-DD date or not a real calendar date, or lo is after hi. |
range.too_narrow | hi is less than 30 days after lo. |
set.count | More than one enum-set predicate. |
set.codes_invalid | codes is empty, or holds a value that is not allowed. |
limits.disclosures, limits.equalities | Too many disclosures or equality predicates. |
nullifier.label_invalid | The scope label does not match the pattern above. |
purpose.missing, purpose.too_long, purpose.invalid_characters | The purpose is absent, over 140 characters, or not one line of plain text. |