Check a certificate
A certificate is Campaign’s signed record of one person’s verified outcome, checkable by anyone once published.
3 things you can check
The signed record
- It proves
- Campaign stands behind it today, under a key it publishes.
- You still trust
- Campaign: its servers and its key.
The fingerprint
- It proves
- The certificate’s exact text, to the letter.
- You still trust
- Whoever served you the text, until the public record matches it.
The public record
- It proves
- That fingerprint was added on its date, and whether it is revoked.
- You still trust
- Campaign’s key that allowed it, and Campaign to send a revocation.
GET https://campaign.udao.org/api/public/credentials/{document number}/verifyGET https://campaign.udao.org
/api/public/ credentials/ {document number}/ verify
What the fingerprint covers
The fingerprint is made from the certificate’s exact text: a short document written once, when the certificate is issued, and served as it was written. That makes the text tamper-evident: change one letter and the fingerprint is another.
It covers
- Who issued it: the organization’s name, its address on Campaign and its verified domain
- What for: the campaign’s title, and the certificate’s title
- When: the day of the outcome, and the day it was issued
- How the outcome was checked, and whether it came from a trial
- Its record number, which the public record knows it by, and a random part kept only in the text
It never covers
- The holder’s name, which they change at any time
- Any amount, or the reward
- The organization’s standing with Udao
- A revocation, which is written beside it
- Its document number, or an id of its holder or its campaign
It is keccak-256, the recipe that makes the fingerprint, over the UTF-8 bytes of the document in RFC 8785 canonical JSON. keccak-256 is not SHA3-256, and their values differ.
The sample below is an earlier version. It covers its document number and an id Campaign gives its holder, in place of its record number and the random part.
SampleThe exact text, 589 bytes as served
{"achieved_at":"2026-09-26T05:32:11.993Z","campaign_id":"c51963e6-12cb-40d4-b09f-838a9913b642","evidence_type":"imported","id":"80757f30-0e4f-5db4-ab92-9d5245dfb9fd","issued_at":"2026-09-26T06:14:12.998Z","issuer":{"kind":"campaign","org_name":"Campaign end to end, operator mode","org_slug":"e2e-amoy","recorded_by":"Campaign"},"recipient":{"credential_id":"80757f30-0e4f-5db4-ab92-9d5245dfb9fd","identity_id":"e205c396-e3c3-48ac-975d-751db5fedfcf"},"schema":"campaign.credential","subject":"Amoy end to end 2026-09-26T05:30:20.064Z","title":"Certificate","version":2,"watermarked":false}The public record
The public record is a register: a contract, which is a program published on a network and run by every copy of it. Its 2 registers are listed below. They run on Polygon, a public network, and until launch on Amoy, Polygon’s test network.
CredentialRegistry, the first test register
Certificates were added here from 11 September 2026 to 4 October 2026. They stay here, and a revocation of any of them is written here.
- Its address
0x02Ffd60f83Fe9953ed197DFDACE9D66Fd5296442Polygon Amoy, chain id 80002 (the network’s own number)Read its source on amoy.polygonscan.com- Campaign’s issuerId
0x40c398…4c2f
CredentialRegistryV2, the register that launches
New certificates are added here, from 4 October 2026.
- Its address
0x0fFd0a5681B285cd1645bF6642B498DF171fE3C7Polygon Amoy, chain id 80002 (the network’s own number)Read its source on amoy.polygonscan.com- Campaign’s issuerId
0x40c398…4c2f
Both registers’ rules
- ERC-721, every move refusedThe standard for numbered entries, every transfer and approval refused: no entry can be sold or handed on. The first test register gives wallets no ERC-5192 signal of it; the register that launches does.
- No burnNothing can delete an entry on either register, Campaign included.
- No upgradeEach keeps the rules it was published with, for as long as it runs.
The register that launches: its rules and reads
- It draws each entry’s picturetokenURI answers a document the register draws from the entry alone. It holds a name, a description, a picture of the certificate and its attributes. The attributes are the fingerprint, the day added, and once revoked its status and the day the revocation reached the record. No link, no name, no title.
- A revocation is marked on the entryrevoke takes no words. The picture gains the revoked stamp with the register’s own UTC day, and the name leads with Revoked. MetadataUpdate (ERC-4906) is written once, so an app that reads the entry again shows it.
- No entry can be sold or handed onERC-5192’s mark is set on every entry, and says nothing of revocation. reissue stays the namespace admin’s move to another address, which Campaign never makes.
- isValid and recordStatusisValid(entry) is true while the entry is not revoked. recordStatus(record number, fingerprint) answers NotOnRecord, OnRecord, Revoked or DoesNotMatch, which wins over Revoked, with the entry, the issuer id and both dates.
- documentURI and contractURIdocumentURI(entry) is the address of its exact text, built from a base written once for Campaign’s namespace. contractURI is the collection’s own document, fixed, with no link.
- Dates in UTCEvery date it holds or draws is a block’s time, shown in UTC. A revocation’s day is the day it reached the register, which can fall after the organization’s decision day when the relay’s minutes cross midnight UTC.
- Holding is not countingownerOf and balanceOf count a revoked entry too. Read isValid.
What each entry holds
| Field | What it holds |
|---|---|
| tokenId | The entry’s number, its tokenId, counted from 1: the key the 5 fields below are read by. |
| issuerId | Campaign’s namespace, the same for every certificate it issues: keccak-256 of the word campaign. |
| credentialId | The record number its exact text carries: 32 random bytes. An earlier entry, like the sample (the test robot’s, added before Campaign sent additions itself), carries the document number’s 16 bytes, then 16 zero bytes. |
| contentHash | The fingerprint. |
| issuedAt | When the entry was added, in seconds, from the block that added it. Not the day the certificate was issued. |
| revokedAt | 0 while it stands. Set once, to the time of its revocation. |
On both registers every field is written once, when the entry is added; only revokedAt moves, once. On the first test register the entry also keeps the address of its exact text (tokenURI), written once. The register that launches derives that address instead (documentURI).
Both write CredentialIssued, CredentialRevoked and CredentialReissued, which the explorer decodes because the source is verified. The register that launches writes CredentialRevoked without a reason, and MetadataUpdate beside it.
How a certificate gets there
- Its holder presses Add to the public record on their passport, and confirms. That yes is recorded, and nothing is sent yet.
- Between 1 and 24 hours later, Campaign sends it to the register as a transaction, which is one signed change. The key that sends it is Campaign’s own. Its permission names the record number, the fingerprint and the certificate’s own address, and lasts 20 minutes. The holder signs nothing and pays nothing, and can stop it until anything is signed.
- The register checks the permission and writes the entry. Campaign reads it back from a final block, and the certificate’s page prints the day.
- Its holder decidesNobody else, the organization included, can start or require it: Campaign sends only what its holder said yes to.
- Never a trial’s certificateNor one whose place did not pass screening.
- Never twiceThe register refuses a second entry for one record number.
The certificate’s own address
Each certificate is added at an address anyone can compute from its entry: nobody holds its key, and it receives nothing.
address = the last 20 bytes of keccak256(abi.encode(
keccak256("campaign.certificate-address.v1"),
chainId,
register,
recordNumber
))A worked example
- The tag
0x684961…c1f5- chainId
- 80002
- register
0x0fFd0a5681B285cd1645bF6642B498DF171fE3C7- recordNumber
0x111111…1111- address
0x41F0987E4457D9fDDbde84f2c30D99c7fce65056
What exists in each state
| Certificate | Public record | Checkable without Campaign | Signed record | Exact text and fingerprint |
|---|---|---|---|---|
| A trial’s | Public recordNever | Checkable without CampaignNothing | Signed recordYes, marked a trial | Exact text and fingerprintYes |
| Not added yet | Public recordNot yet | Checkable without CampaignNothing | Signed recordYes | Exact text and fingerprintYes |
| Added | Public recordYes | Checkable without CampaignIts fingerprint, date and status | Signed recordYes | Exact text and fingerprintYes |
| Revoked after it was added | Public recordYes, with the revocation once relayed | Checkable without CampaignAll of it | Signed recordYes; its bit on the revocation list says Revoked | Exact text and fingerprintYes |
| Revoked before an adding began | Public recordNever | Checkable without CampaignNothing | Signed recordYes; its bit on the revocation list says Revoked | Exact text and fingerprintYes |
Check it yourself: 4 levels
Each level checks what the one before it left to trust. None of them needs ownerOf, the call that names the address holding an entry: the entry is found by its record number.
SampleThe sampleEvery value below is a real certificate: the first test register’s entry 2, added by our end-to-end test robot.
- Document number
80757f30-0e4f-5db4-ab92-9d5245dfb9fd- Its entry
- 2, on the first test register
- Fingerprint
0xd3fb47…9aff- Added
- 26 September 2026, 06:15:02 UTC
- Its page
- campaign.
udao.org/ p/ e2e-golden-participant/ 80757f30-0e4f-5db4-ab92-9d5245dfb9fdcampaign.udao.org/p/…45dfb9fdOpen
- Its entry names campaign.invalid, the robot’s test address, for its text, for good. So the recipe reads the text on campaign.udao.org by the document number, and never needs tokenURI.
- Its organization is the robot’s test organization, still in review, which is why its page says Udao has not reviewed it.
- No revoked sample exists on this register yet: a revocation is read with the code and the state table above.
Level A. In a browser, about 5 minutes
- It proves
- That this fingerprint was added at that time, and whether it is revoked.
- You still trust
- The explorer (a site Campaign does not run), Campaign’s key, and Campaign to send a revocation.
- Open the register’s Read Contract page. Its address must match the one drawn below, character for character, beside its verified source.
- In tokenIdByCredentialId, enter the record number, read from the exact text (for the earlier sample, 0x, the document number’s 32 hex digits, then 32 zeros). It answers the entry’s number.
- In credentials, enter that number. contentHash must equal the certificate’s fingerprint, issuerId Campaign’s, and revokedAt 0.
amoy.polygonscan.comRead Contract
- Contract
0x02Ffd6…6442must match, character for character
tokenIdByCredentialId
- credentialId
0x80757f…0000- uint256
- 2the entry’s number
credentials
- tokenId
- 2
- issuerId
- 0x40c398…4c2fCampaign’s, always
- credentialId
- 0x80757f…0000
- contentHash
- 0xd3fb47…9affmust equal the certificate’s fingerprint
- issuedAt
- 1790403302when it was added: 26 September 2026, 06:15:02 UTC
- revokedAt
- 00: not revoked
SampleDrawn after the explorer’s Read Contract page, with the sample’s values
Level B. Recompute the fingerprint
- It proves
- That the text you hold is the text that was added.
- You still trust
- Only what level A or D trusts.
- Fetch the exact text by its document number, and keep the copy.
- Hash the bytes exactly as served, never parsed and written out again, with keccak-256.
- The answer must equal contentHash.
SampleFetch, keep and fingerprint the exact text
# Keep the copy: it is the certificate's exact text. curl -s https://campaign.udao.org/api/public/credentials/80757f30-0e4f-5db4-ab92-9d5245dfb9fd/document -o exact-text.json wc -c < exact-text.json | tr -d ' ' # keccak-256 needs OpenSSL 3.2 or later. It is not SHA3-256: never -sha3-256. openssl dgst -keccak-256 -r exact-text.json | cut -c1-64 | sed 's/^/0x/'
589 0xd3fb47fcb8c1961c16cbf75976130eb37929cb15b5fcab0acfa62d6e0a2f9aff
Level C. Verify the signed record
- It proves
- That Campaign states this today, under its published key, and whether it is revoked.
- You still trust
- Campaign, which signs at each read and serves the list, with a key on its own site.
- Fetch the record and the key it names, and check the signature. The key’s controller must be the record’s issuer, Campaign’s own address, which lists the key.
- Fetch the list the record’s credentialStatus names, under the issuer’s own address. Check its signature with the same key, never a key the list names. Its issuer must be the record’s, its type BitstringStatusListCredential, its statusPurpose revocation, and now must fall between its validFrom and validUntil.
- Drop the leading u, decode the rest as base64url, gunzip it, and require at least 131,072 bits.
- Read the bit at statusListIndex, counting the left-most bit as 0. A 1 means Revoked; a 0 means not revoked.
- A list you cannot fetch or check means the status is unknown, never valid.
- For a certificate on the public record, also read revokedAt, as in level A or D. It is Revoked if either says so. On the register that launches, isValid(entry) answers it in one call.
The record follows the Open Badges 3.0 data model. It carries 2 proofs made with one key, in the W3C eddsa-rdfc-2022 and eddsa-jcs-2022 suites: check either one. Its status is a W3C Bitstring Status List entry. In Campaign’s own test, the Digital Credentials Consortium’s verifier-core 1.0.0-beta.11 checks the record’s signature and reads its bit on the list.
SampleCheck the signature against the published key
// check-signature.mjs, Node 18 or later: nothing to install.
import { createHash, createPublicKey, verify } from "node:crypto";
const record = await (await fetch("https://campaign.udao.org/api/public/credentials/80757f30-0e4f-5db4-ab92-9d5245dfb9fd/verify")).json();
const key = await (await fetch("https://campaign.udao.org/api/public/credentials/signing-key")).json();
// RFC 8785: keys sorted, no spaces.
const jcs = (v) => v === null || typeof v !== "object" ? JSON.stringify(v)
: Array.isArray(v) ? "[" + v.map(jcs).join(",") + "]"
: "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}";
const sha256 = (v) => createHash("sha256").update(jcs(v)).digest();
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const base58 = (text) => {
let n = 0n;
for (const c of text) n = n * 58n + BigInt(ALPHABET.indexOf(c));
const hex = n.toString(16);
const zeros = text.length - text.replace(/^1+/, "").length;
return Buffer.concat([Buffer.alloc(zeros), Buffer.from(hex.length % 2 ? "0" + hex : hex, "hex")]);
};
// eddsa-jcs-2022: Ed25519 over the 2 digests, the proof's options first.
const { proof: proofs, ...document } = record;
const { proofValue, ...options } = proofs.find((one) => one.cryptosuite === "eddsa-jcs-2022");
const raw = base58(key.publicKeyMultibase.slice(1)).subarray(2); // after 0xed01
const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
// The key is the issuer's: its controller is the record's issuer.
const valid = options.verificationMethod === key.id && key.controller === record.issuer.id
&& verify(null, Buffer.concat([sha256(options), sha256(document)]), publicKey, base58(proofValue.slice(1)));
console.log(valid ? "valid" : "INVALID");
// Its status is an entry on the revocation list: the next check reads it.
console.log(record.credentialStatus.type);A shell has no Ed25519, so this level is in Node and Python.
valid BitstringStatusListEntry
SampleRead the certificate’s bit on the revocation list
// check-status.mjs, Node 18 or later: npm install jsonld
import { createHash, createPublicKey, verify } from "node:crypto";
import { gunzipSync } from "node:zlib";
import jsonld from "jsonld";
const record = await (await fetch("https://campaign.udao.org/api/public/credentials/80757f30-0e4f-5db4-ab92-9d5245dfb9fd/verify")).json();
const entry = record.credentialStatus;
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const base58 = (text) => {
let n = 0n;
for (const c of text) n = n * 58n + BigInt(ALPHABET.indexOf(c));
const hex = n.toString(16);
const zeros = text.length - text.replace(/^1+/, "").length;
return Buffer.concat([Buffer.alloc(zeros), Buffer.from(hex.length % 2 ? "0" + hex : hex, "hex")]);
};
// RDF Dataset Canonicalization, the eddsa-rdfc-2022 suite's.
const nquads = (doc) => jsonld.canonize(doc, { algorithm: "RDFC-1.0", format: "application/n-quads", safe: true });
const sha256 = (text) => createHash("sha256").update(text).digest();
let status = "status unknown"; // until every check below passes
try {
const list = await (await fetch(entry.statusListCredential)).json();
// The published key, as in step 1: never the one the list names.
const key = await (await fetch("https://campaign.udao.org/api/public/credentials/signing-key")).json();
const now = new Date();
const { proof, ...unsecured } = list;
const { proofValue, ...options } = proof;
const raw = base58(key.publicKeyMultibase.slice(1)).subarray(2); // after 0xed01
const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
const data = Buffer.concat([
sha256(await nquads({ "@context": unsecured["@context"], ...options })),
sha256(await nquads(unsecured)),
]);
// Signed with the record's key, by its issuer, at its issuer's address;
// the list's type, purpose and dates; its proof.
const valid = key.id === proof.verificationMethod && key.controller === record.issuer.id
&& list.issuer.id === record.issuer.id && entry.statusListCredential.startsWith(record.issuer.id)
&& proof.cryptosuite === "eddsa-rdfc-2022"
&& list.type.includes("BitstringStatusListCredential")
&& list.credentialSubject.statusPurpose === "revocation"
&& new Date(list.validFrom) <= now && now <= new Date(list.validUntil)
&& verify(null, data, publicKey, base58(proofValue.slice(1)));
console.log(valid ? "valid" : "INVALID");
// Drop the u, decode base64url, gunzip: at least 131,072 bits.
const bits = gunzipSync(Buffer.from(list.credentialSubject.encodedList.slice(1), "base64url"));
console.log(bits.length * 8);
if (valid && bits.length * 8 >= 131072) {
// The left-most bit is 0. A 1 is revoked.
const index = Number(entry.statusListIndex);
const bit = (bits[Math.floor(index / 8)] >> (7 - (index % 8))) & 1;
status = bit === 1 ? "revoked" : "not revoked";
}
} catch {}
console.log(status);valid 131072 not revoked
The list is one file for every certificate, so fetching it names none. It carries bits and Campaign’s name: no document number, date or reason. Campaign keeps no record of who fetches it; the host it runs on keeps its own request logs for a while.
Level D. Any node, without the explorer
- It proves
- What level A proves, with nobody between you and the network.
- You still trust
- The node you ask (run your own), Campaign’s key, and Campaign to send a revocation.
- Ask any Amoy node for its chain id. It must be 80002.
- Call tokenIdByCredentialId, then credentials, at the register’s address, and match them as in level A.
SampleRead the entry from an Amoy node
NODE=https://polygon-amoy-bor-rpc.publicnode.com
REGISTER=0x02Ffd60f83Fe9953ed197DFDACE9D66Fd5296442
rpc() {
curl -s "$NODE" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"'"$1"'","params":'"$2"'}' \
| sed -E 's/.*"result":"0x([0-9a-f]*)".*/\1/'
}
call() { rpc eth_call '[{"to":"'"$REGISTER"'","data":"'"$1"'"},"latest"]'; }
printf '%d\n' "0x$(rpc eth_chainId '[]')"
# tokenIdByCredentialId: the record number, from the exact text (this earlier sample's: its document number's 32 hex digits, then 32 zeros).
ENTRY=$(call 0xa0daaf6c80757f300e4f5db4ab929d5245dfb9fd00000000000000000000000000000000)
echo "$ENTRY"
# credentials(entry): issuerId, credentialId, contentHash, issuedAt, revokedAt.
call "0xe0574e3f$ENTRY" | fold -w 6480002 0000000000000000000000000000000000000000000000000000000000000002 40c398d6e1fb1e64fa89a0f2f232df777e9e84f3ef558102d564475b8a404c2f 80757f300e4f5db4ab929d5245dfb9fd00000000000000000000000000000000 d3fb47fcb8c1961c16cbf75976130eb37929cb15b5fcab0acfa62d6e0a2f9aff 000000000000000000000000000000000000000000000000000000006ab762e6 0000000000000000000000000000000000000000000000000000000000000000
Line by line
- 80002the chain id: Amoy
- 000000…0002the entry’s number
- 40c398…4c2fissuerId: Campaign’s
- 80757f…0000credentialId: the record number
- d3fb47…9affcontentHash: the fingerprint
- 000000…62e6issuedAt: 26 September 2026, 06:15:02 UTC
- 000000…0000revokedAt: 0, not revoked
For an employer’s system
5 public reads, with no API key and no account, which a browser on any site can read. The first 3 are limited to 120 a minute for one caller. The key and the revocation list are not, and a shared cache holds the key for 5 minutes and the list for 1.
GET/The signed record, as application/vc+ld+json. The certificate’s own address answers the same document to a request whose Accept names JSON.api/ public/ credentials/ {document number}/ verify GET/The exact text, as written when the certificate was issued.api/ public/ credentials/ {document number}/ document GET/Its public record, read when you ask and kept a minute, unsigned: status is matches, revoked, does_not_match, revocation_pending or unreadable, with checked_at. Only once the entry answers the certificate’s record number and Campaign’s issuer does it say more than unreadable. An entry at the certificate’s own address also carries chain_id, registry, record_number, token_id and tx_hash.api/ public/ credentials/ {document number}/ chain GET/The public key every proof names, as a Multikey. Its controller, Campaign’s own address, answers a request whose Accept names JSON with its controller document, which lists the key.api/ public/ credentials/ signing-key GET/The revocation list, signed by Campaign: one bit for each certificate, 1 once it is revoked. A copy of a record you were given still finds its bit after its holder hides the certificate.api/ public/ credentials/ status/ 1
The first 3 answer 404 credential_not_found alike for:
- an unpublished passport
- a certificate its holder hid
- an organization Udao has not approved, other than its own test organization
- a number that names nothing
The second also answers it for a certificate issued before exact texts were kept. The third also answers it for one that is not on the public record.
The list answers 404 status_list_not_found to any number but 1.
Revocations and reissues
The organization that issued a certificate decides to revoke it:
- Its verifier, during the campaign’s review window
- Its owner, after it, even once settled
- Its system, through the API
A reward already reserved stays with the person. The register’s revoke and the outcome.revoked your webhook receives are the same act, said by 2 systems.
- At onceCampaign’s pages say Revoked, with the date. The certificate’s bit on the revocation list turns on for good: anyone checking it sees it within a minute.
- Within minutesCampaign’s relay sends revoke to the register with Campaign’s operator key each minute. It counts it done once a final block shows revokedAt, its date. A send not seen in a block is sent again after 15 minutes.
- Its reasonNone: the first test register’s reason is left empty, the register that launches takes none, and the record behind it stays with Campaign.
Reading a revocation on the first test register
- revokedAtIn credentials(entry): 0 while it stands, the time it was revoked after.
- The CredentialRevoked logIts topic is below. The explorer’s Read Contract answers revokedAt, and its event log decodes the register’s events, because the source is verified.
- Holding is not countingownerOf and balanceOf still count a revoked certificate. Read revokedAt.
- Not in wallet appsWallet and marketplace apps, and the explorer’s page for the single item, show a revoked certificate unchanged.
- CredentialRevoked topic
0x434cf4…a4ed
Reading a revocation on the register that launches
- isValidfalse once the entry is revoked; revokedAt in credentials(entry), as on the first register.
- MetadataUpdateWritten once, with the revocation, so an app that reads the entry again shows its stamp. A copy an app keeps can stay unchanged until it reads the entry again.
- CredentialRevoked topic on the register that launches
0xce470a…fbbf
An entry moves to another address only by reissue, which the namespace’s admin alone can call and which writes CredentialReissued. Nothing in the product calls it today.
Who holds which role
On the test network one key holds both admin roles. On Polygon itself a multisig holds them from launch, when every role is accounted for.
CredentialRegistry, the first test register
- DEFAULT_ADMIN_ROLEGrants and removes every role, its own too.
- Udao’s deployer key
0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD - ISSUER_ADMINOpens an issuer namespace, and can hand one to a new admin.
- Udao’s deployer key
0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD - PLATFORM_SIGNERSigns the permission an adding needs.
- Nobody: this register adds nothing more.
- Admin of the campaign namespaceRevokes and reissues its entries, and can hand the namespace on.
- Campaign’s operator
0x3639ddBB7Dab03b38Af34BDe80E10CC7E42D55De
It is retired: its admin roles stay with the deployer key, and no multisig takes them.
CredentialRegistryV2, the register that launches
- DEFAULT_ADMIN_ROLEGrants and removes every role, its own too.
- Udao’s deployer key
0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD - ISSUER_ADMINOpens an issuer namespace, and can hand one to a new admin.
- Udao’s deployer key
0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD - PLATFORM_SIGNERSigns the permission an adding needs.
- Campaign’s permission signer
0xcc75638b0B025ffb76b93783aD63D8657cDC74CE - Admin of the campaign namespaceRevokes and reissues its entries, and can hand the namespace on.
- Campaign’s operator
0x3639ddBB7Dab03b38Af34BDe80E10CC7E42D55De
Read off both registers on 4 October 2026. No role can change an entry’s facts.
What adding makes public
It shows
- The certificate’s own address, computed from its record number: nobody holds its key, it receives nothing, and it holds this certificate alone.
- Its record number, its fingerprint, the time it was added, and Campaign’s issuer id.
- The address of its exact text, which answers only while its holder’s passport is published and the certificate shown.
- For a certificate also published from a small listed campaign: its campaign’s title, which can narrow it to that campaign’s few rewarded people.
- For a revocation: its time, with no reason; on the register that launches, the picture’s revoked stamp with that day.
- On the register that launches, a picture and words any app reading the entry can show, drawn from these facts alone: it adds no fact.
It never shows
- A name
- An email address
- The passport’s address
- Evidence or notes
- An amount
A certificate nobody added leaves nothing on the public record at all.
What changes at launch
- CredentialRegistryV2, the register that launches, runs here first. The same verified source goes to Polygon itself, chain id 137, at a new address, and its entries count from 1 again there. Its explorer is polygonscan.com.
- Entries on the test network are not carried over. Both of its registers stay described here, with their dates.
- Every addition Campaign sends is at its own computed address, as on the test network.
- A multisig takes the admin roles from the single deployer key.
The limits
- It proves the statement, not the outcomeThe record proves the organization’s statement was added and has not changed since. Not that the outcome happened, and not that the person showing it is its holder: the name is on no record.
- Before it is added, every check rests on usThe signed record and the exact text come from Campaign’s servers. Until the public record matches the fingerprint, they check Campaign against itself.
- Holders: keep a copy of the exact textIts address answers only while your passport is published and the certificate shown. Your passport keeps a copy for you to take, and a copy you kept still proves itself against the public record.
- This is a test networkAmoy is Polygon’s test network, and its entries are not carried to launch.
Glossary
| On our pages | Technical term |
|---|---|
| The public record | The register: CredentialRegistry, the first test register, then CredentialRegistryV2, the register that launches |
| Its entry | One token of the register, under ERC-721 |
| The entry’s number | Its tokenId |
| The fingerprint | contentHash: keccak-256 of the exact text |
| The exact text | The canonical JSON document, RFC 8785 |
| A certificate | A credential, as the reads’ paths and codes name it |
| The document number | The certificate’s id, printed on it as Document number and ending its page’s address |
| The record number | The credential id on the register: 32 random bytes, in the exact text |
| Added on | The UTC day of the block that added it |
| Revoked | revokedAt, set once; outcome.revoked for a partner |
| Record reference | The hash of the transaction that added it |
| Its published rules | The contract’s verified source |
| Testnet | Amoy, Polygon’s test network |
| Nobody can change it, not even us | Every field written once, with no setter and no upgrade |
| Signed by Campaign | 2 DataIntegrityProofs with one key, eddsa-rdfc-2022 and eddsa-jcs-2022 |
| The revocation list | A W3C Bitstring Status List: one bit for each certificate |