The earlier chapters turned a DAC plant into data: facility records, capture records, sorbent records and storage events, exchanged through a common API and produced by auditable operating protocols. None of that is yet a removal claim. A tonne of CO2 becomes something a company can buy and retire only after an independent party has checked the evidence, a registry has issued a serialized unit for it, and every later change of ownership or retirement can be traced back to the same verified quantity. This chapter covers that last stretch of the chain.
In WIA-ENE-050 the work is split between the Verification Protocol DAC-MRV-001 (PHASE-3-PROTOCOL §5.1), the verification report and carbon credit schemas (PHASE-2-DATA §6–7), and the verifier, registry, Verifiable Credential and blockchain integrations (PHASE-4-INTEGRATION §5–6). The spec describes each piece, but it leaves several gaps between them: how uncertainty turns into the verified number, what exactly is signed, and which ledger is authoritative once a credit exists in more than one place. The chapter follows the chain in order and marks where the book recommends practice beyond the spec.
Two caveats apply throughout. First, the spec's examples name real operators, verification bodies and registries, and attach invented numbers to them. The examples here use the fictional facility dac-example-reykjanes-01 and a fictional "Example Verification Body", and every number is labelled as illustrative. Second, registries publish and revise their own methodologies and rules. This chapter describes how WIA data maps onto a registry workflow in general terms. It does not restate any registry's current rulebook, which the project developer must read in its current edition.
MRV stands for monitoring, reporting and verification. The three words describe different activities with different owners. Monitoring is the operator measuring the plant. Reporting is the operator publishing or submitting what was measured. Verification is an independent body checking that the report is supported by the measurements. DAC-MRV-001 gives each activity its own cadence.
PHASE-3-PROTOCOL §5.1.1 defines four monitoring tiers. Table 6-1 lists what the spec puts in each tier and which WIA records carry that information. The last column is the book's reading of how the tier becomes data.
| Tier | Contents per the spec | Carrying records | How it becomes evidence |
|---|---|---|---|
| Continuous (real-time) | CO2 capture rate, energy consumption, ambient conditions, equipment status | Real-time capture records (PHASE-2-DATA §3) | Raw series, retained unaltered with QC flags (Chapter 5) |
| Daily | Sorbent condition, performance metrics, safety checks, backup verification | Sorbent records, checklist records, daily summaries | Daily summaries are signed or hashed so later edits are detectable |
| Monthly | Aggregated capture totals, energy efficiency, sorbent degradation trends, maintenance | Aggregates derived from capture and energy records; maintenance records | Each aggregate names the raw records and the algorithm version it was computed from |
| Annual | Third-party verification, full system audit, regulatory review, long-term trends | Verification report (PHASE-2-DATA §6) | The verification report references everything above by ID and digest |
The tiers are nested: each is only as good as the one beneath it. The practical rule is that no aggregate may exist without a pointer to its inputs. If a monthly total cannot be recomputed from retained raw data, a verifier must treat it as an unsupported assertion. This is why FOUNDATION §5.1 asks for raw data to be kept for seven years and verification records permanently (Chapter 2).
The spec sets the verification frequency at "annual minimum" (FOUNDATION §6.2.2). Nothing prevents more frequent verification. A plant that sells removals forward, or that wants to issue credits quarterly, can run quarterly verification periods. The data model already supports this, because a verification report carries an explicit period with start and end dates rather than a year.
PHASE-3-PROTOCOL §5.1.2 defines four reporting streams, each with its own audience. Table 6-2 summarizes them together with the access tier that Chapter 7 assigns to published data.
| Stream | Cadence | Contents per the spec | Audience and access |
|---|---|---|---|
| Internal | Daily | Operations dashboard, incident logs, production summary | Operator staff; private |
| Public | Monthly | Total CO2 captured (t), energy (MWh), uptime (%), "carbon intensity" (MWh/t CO2), through a WIA-ENE-050 compliant API | Anyone; public |
| Regulatory | Annual | Emissions/removals report, environmental impact, safety incidents, compliance certification | Competent authorities; partner or private |
| Verification | Annual | Third-party audit report, credit issuance request, methodology documentation | Verifier and registry; partner |
One label in the public stream needs correcting before it is published. The spec calls MWh per tonne of CO2 "carbon intensity". That quantity is specific energy, the term used in the facility and capture records. Carbon intensity usually means emissions per unit of output, for example tonnes of CO2e emitted per tonne removed. Both numbers are useful, and a public report can carry both, but they must not share a name. The book recommends publishing specificEnergy (MWh/t) and, separately, the ratio of life-cycle emissions to gross capture, which is the quantity buyers actually ask about.
The public monthly figures are also gross and unverified. A public dashboard should label them "captured, unverified" and show "verified net removal" only for closed periods.
PHASE-3-PROTOCOL §5.1.3 describes verification in five steps: document review, on-site inspection, data analysis, verification statement and certification issuance. The steps follow the usual structure of GHG verification under ISO 14064-3, the international standard for validating and verifying GHG statements, and the body doing the work is normally accredited against ISO 14065, which sets requirements for bodies that validate and verify environmental information. The WIA standard does not replace either document. Its contribution is to make the evidence arrive in a predictable, machine-checkable form.
Figure 6-1 shows the whole sequence from the plant to a retired credit. Each arrow is a message or a record, and each column is a separate organization.
OPERATOR PLANT MRV PLATFORM VERIFIER REGISTRY BUYER
| | | | |
|-- capture recs -->| | | |
|-- storage evts -->| status: pending-verification | |
| | | | |
| (period closes)| | | |
| |-- verification --->| | |
| | package + digest | 1 document review | |
|<------------------------ site visit ---| 2 on-site | |
| |<-- data queries ---| 3 data analysis | |
| |<-- report + VC ----| 4 statement | |
| | (verify-...) | | |
| |-- issuance request (report ID) -------->| 5 issuance |
| |<-- serial range, status: issued --------| |
| | | |-- transfer ---->|
| | | |<-- retire ------|
| |<-- status: retired, beneficiary --------| |
| | (update credit record, VC status, token) |
Two design choices are built into the figure. The verifier receives a package with a digest, not live database access, so that what was verified can later be identified exactly. And status flows back from the registry to the MRV platform, not the other way round. Section 6.4 explains why.
| Step (spec) | What the verifier examines | WIA inputs | Output record |
|---|---|---|---|
| 1. Document review | Facility design, operating procedures, QA/QC procedures, historical performance, calibration records | Facility record, protocol documents, calibration and maintenance records | Findings of category observation or non-conformance |
| 2. On-site inspection | Equipment walk-through, meter calibration check, operator interviews, witnessed procedures | Checklist records, meter serials linked to calibration certificates | Findings; photographs and notes held by the verifier |
| 3. Data analysis | Calculation methods, cross-checks between sources, anomaly statistics, uncertainty | Capture, energy and storage records; QC flags; aggregation code version | Recomputed totals; uncertainty assessment (section 6.3) |
| 4. Verification statement | Verified removal amount, confidence level, uncertainty range, conformance, recommendations | Results of steps 1–3 | Verification report (verify-…) with conclusion |
| 5. Certification issuance | Credential issued, registry notified, public report published | Verification report | Verifiable Credential (6.5); registry issuance request (6.4) |
The third step is where a data standard pays off most. With records that follow PHASE-2-DATA, the verifier can run the operator's own consistency checks (Chapter 2, section 2.7) and QC rules (Chapter 5, section 5.7) and recompute the period totals independently.
PHASE-4-INTEGRATION §6.1.1 sketches an automated export: gather facility information, capture data, energy data, storage events, calibration records and maintenance logs for a period, wrap them with a version and the standard's identifier, attach a checksum and post the result to the verifier's portal. The idea is sound. The sample code has three problems that an implementer should not copy.
package. In strict-mode JavaScript, which includes every ES module, package is a reserved word, so the code fails with a syntax error. Rename the variable.calculateChecksum(data) is not defined, and hashing the output of JSON.stringify gives different bytes for the same data whenever key order or number formatting differs. Hash a canonical form instead. The JSON Canonicalization Scheme (RFC 8785) exists for exactly this purpose, and SHA-256 is the natural hash.A package manifest in that shape looks like this. The digests are shortened placeholders.
{
"packageId": "vpkg-example-reykjanes-01-2025",
"standard": "WIA-ENE-050",
"standardSlug": "direct-air-capture",
"schemaVersion": "1.0.0",
"facilityId": "dac-example-reykjanes-01",
"period": { "startDate": "2025-01-01", "endDate": "2025-12-31" },
"createdAt": "2026-01-15T09:00:00Z",
"files": [
{ "path": "facility.json", "records": 1, "sha256": "9f2c…" },
{ "path": "capture/2025-*.jsonl", "records": 525600, "sha256": "41ab…" },
{ "path": "storage-events.jsonl", "records": 48, "sha256": "c07e…" },
{ "path": "calibration.jsonl", "records": 12, "sha256": "77d1…" },
{ "path": "maintenance.jsonl", "records": 96, "sha256": "e5a0…" },
{ "path": "aggregates/monthly.json", "records": 12, "sha256": "0b3f…",
"derivedFrom": ["capture/2025-*.jsonl"], "algorithm": "monthly-agg@2.3.1" }
],
"canonicalization": "RFC 8785",
"manifestDigest": "sha256:5d8e…"
}
The record count of 525,600 is simply one record per minute for a 365-day year, used here for illustration. The derivedFrom and algorithm fields make the "no aggregate without inputs" rule from section 6.1 explicit. Once the verifier has accepted the package, its manifestDigest goes into the verification report (as an extension field) and later into the Verifiable Credential. From then on anyone can check that a credential refers to exactly this evidence.
FOUNDATION §4.4 sets the targets the verifier checks against: CO2 capture measured to ±5 % uncertainty, energy consumption to ±3 %, storage verification at better than 95 % confidence, and hourly or finer temporal resolution. The protocol text repeats the idea in its verification statement: a verified amount, a confidence level "e.g., 95 %" and an uncertainty range "±X %".
These are useful targets, but as written they are ambiguous in three ways. The spec does not say whether ±5 % is a standard uncertainty or an expanded one. It does not say whether the percentage is relative to gross capture or to net removal. And it does not say what happens when a period misses the target. Section 6.3 turns the targets into a calculation that answers all three.
Every term in the net-removal formula from Chapter 1 is measured or estimated with some uncertainty. The verified amount should reflect that uncertainty, and the direction of any adjustment should be conservative: when in doubt, credit fewer tonnes, not more. This section shows one defensible way to do both. It follows the approach of the Guide to the Expression of Uncertainty in Measurement (GUM, published as ISO/IEC Guide 98-3). Registries and methodologies may prescribe their own procedure, and where they do, theirs governs.
Net removal is a sum and difference of terms:
N = S − E − M − L
uc(N) = √( u(S)² + u(E)² + u(M)² + u(L)² ) (independent terms)
S = CO2 stored (custody-transfer meter) · E = energy-related emissions · M = embodied/LCA emissions allocated to the period · L = losses and leakage
For independent terms, standard uncertainties in tonnes combine as the root of the sum of squares. Two points are easy to get wrong. First, the formula adds absolute uncertainties in tonnes, not percentages. A ±10 % uncertainty on a small term contributes far less than ±2 % on the large stored quantity. Second, terms that share an input are not independent. If both the energy emissions and a loss estimate were derived from the same meter, their errors move together and must be combined with a covariance term, or conservatively added linearly.
Meter specifications are usually stated as limits ("accurate to ±2 %") rather than standard uncertainties. Under the GUM, a limit with no further information is treated as a rectangular distribution, and its standard uncertainty is the half-width divided by √3.
Suppose the fictional plant closes an annual period with the following figures. All values are invented for the example and are not typical of any real facility.
| Term | Value (t CO2) | Basis of uncertainty | Standard uncertainty (t) |
|---|---|---|---|
| S — stored, custody-transfer meter | 1,000.0 | ±2 % meter limit, rectangular: 20 / √3 | 11.5 |
| E — energy emissions (2,000 MWh × 0.04 t/MWh) | 80.0 | Meter and emission factor combined, estimated 10 % | 8.0 |
| M — embodied emissions allocated to period | 40.0 | LCA estimate, 25 % | 10.0 |
| L — losses between meter and storage | 5.0 | Engineering estimate, 50 % | 2.5 |
| N — net removal | 875.0 | Root sum of squares | 17.4 |
The combined standard uncertainty is √(11.5² + 8.0² + 10.0² + 2.5²) ≈ 17.4 t. With a coverage factor k = 2, which corresponds to roughly 95 % coverage for a normal distribution, the expanded uncertainty is about 34.8 t, or 4.0 % of the net removal. Against the FOUNDATION §4.4 target, the capture measurement itself (±2 % limit) and the net result (±4.0 % expanded) both lie within ±5 %.
The target should be stated relative to net removal. The deductions carry their own uncertainty while the net quantity is smaller than the gross, so a plant with high energy or embodied emissions can miss the target on net removal even when every meter is within specification.
A verified amount of 875 t would be the best estimate. It would also be too high about half the time. The conservative practice is to credit a lower confidence bound. Using a one-sided 95 % bound (k ≈ 1.645 for a normal distribution):
verifiedAmount = N − 1.645 × uc(N) = 875.0 − 1.645 × 17.4 ≈ 846.4 → 846 t (rounded down)
The deduction of about 29 t is the price of uncertainty, and it gives the operator a direct incentive to reduce it. Better metering, a better-documented LCA or a measured loss term instead of an estimate each shrink the deduction. This is the connection that makes the ±5 % target meaningful. A period that misses the target is not rejected; it simply receives a larger deduction. If the uncertainty is so large that the evidence cannot support any confident positive number, the conclusion becomes not-verified.
The verification report schema has a single numeric uncertainty field described as a "measurement uncertainty percentage". On its own that number cannot be interpreted. The book recommends always filling it with the expanded relative uncertainty of net removal, and adding an extension that states the basis. The report for the worked example would look like this:
{
"reportId": "verify-example-reykjanes-01-2025",
"facilityId": "dac-example-reykjanes-01",
"period": { "startDate": "2025-01-01", "endDate": "2025-12-31" },
"verifiedAmount": 846,
"uncertainty": 4.0,
"verifier": {
"organization": "Example Verification Body",
"accreditation": "ISO 14065 accreditation (reference number of the accreditation body)",
"auditor": "lead-auditor-07"
},
"standards": ["ISO 14064-2", "ISO 14064-3"],
"methodology": "Registry methodology name and version, as applied",
"findings": [
{ "category": "observation", "severity": "minor",
"description": "Loss term L estimated from engineering data; direct measurement recommended." }
],
"conclusion": "verified",
"verificationDate": "2026-02-20",
"nextVerificationDue": "2027-02-20",
"x-uncertaintyBasis": {
"bestEstimate": 875.0,
"combinedStandardUncertainty": 17.4,
"expandedUncertainty": { "k": 2, "value": 34.8, "relativeTo": "net", "percent": 4.0 },
"conservativeBound": { "oneSidedConfidence": 0.95, "value": 846.4 },
"method": "GUM (ISO/IEC Guide 98-3), independent terms"
},
"x-packageDigest": "sha256:5d8e…"
}
Note that standards lists ISO 14064-2 and ISO 14064-3, not ISO 27916. The spec's examples cite ISO 27916 as the verification standard, but ISO 27916 covers CO2 storage in association with enhanced oil recovery. It is not a general standard for verifying DAC removals, and it should appear only if the storage in question actually is EOR-associated. The value of methodology should name the registry methodology and its version exactly as the registry publishes it.
A verified quantity is still not a tradable unit. Units are created by a registry: an organization that keeps accounts, issues serialized units against approved evidence, records transfers between accounts and records retirements. FOUNDATION §6.2.1 names four crediting programmes: Puro.earth, Verra's Verified Carbon Standard, Gold Standard and the American Carbon Registry. The carbon credit schema allows these four values in its registry enum, plus Other.
PHASE-4-INTEGRATION §5.1 uses Puro.earth as its worked example. Puro.earth issues CO2 Removal Certificates (CORCs), each representing one tonne of CO2 removed and stored. The spec's registration and issuance calls are shown against a base URL of the form api.puro.earth/v1. That URL and those payloads are illustrative. They are not a published registry API, and the real procedures involve the registry's own documentation, audits and account system. What carries over is the sequence of stages, which is similar across programmes:
| Stage | What happens | WIA artefact | Status after |
|---|---|---|---|
| Project registration | Developer describes the facility, removal method, storage arrangement and chosen methodology | Facility record; storage plan; registry project ID stored in an extension | Registered |
| Validation | An independent body checks the project design against the methodology before crediting starts | Validation findings, kept alongside the facility record | Validated / eligible |
| Monitoring and verification | Each crediting period is verified (section 6.2) | Verification report verify-… | Verified quantity available |
| Issuance | Registry creates serialized units in the developer's account | Carbon credit record(s) with serialNumber; status: issued | Issued |
| Transfer | Units move to a buyer's account | owner updated; status: transferred | Transferred |
| Retirement | Holder permanently uses the units for a claim, naming a beneficiary | status: retired; retirement details in an extension | Retired (final) |
| Cancellation | Units withdrawn, for example after an error or a reversal | status: cancelled, with a reason | Cancelled (final) |
The status enum of the carbon credit schema (issued, transferred, retired, cancelled) covers the last four stages. It does not include the earlier project states, which belong to the facility rather than the credit.
Registries use their own field names, and those names change between systems and versions. The mapping below is therefore expressed in terms of generic registry concepts that every programme has in some form.
| WIA field | Registry concept | Implementation note |
|---|---|---|
facilityId | Project or facility identifier | The registry assigns its own project ID; store it (e.g. x-registryProjectId) and never reuse facilityId as a registry key |
Verification report period | Monitoring or crediting period | Periods must not overlap for the same facility and storage stream |
verifiedAmount | Quantity eligible for issuance | The registry may apply its own deductions or buffer contributions, so issued quantity can be lower |
vintage | Vintage | The spec defines vintage as the year of removal; confirm the registry's definition, since programmes differ |
serialNumber | Serial number or serial block | Store the registry's value verbatim; do not generate your own |
methodology | Methodology name and version | Copy exactly; a version change can change eligibility |
verifier.organization, accreditation | Validation/verification body | The registry normally keeps its own list of approved bodies |
owner | Account holder | Mirror only; the registry account is authoritative |
status | Unit status | Mirror only; updated from registry notifications or polling |
| (none) | Retirement beneficiary and purpose | Add an extension, e.g. x-retirement with beneficiary, purpose and date |
The spec is inconsistent about serials. In the Puro example, issuing 1,000 t returns a list of per-tonne serial numbers. In the WIA endpoint POST /credits/issue (PHASE-4 §2.1.2), issuing 1,000 t returns a single credit with one serialNumber and amount: 1000. Both are workable, but a system has to choose. The book recommends treating a credit record as a serial block: one record per issuance, carrying the first and last serial of the block in an extension. Transfers and retirements that split a block create new records for the split parts. This mirrors how registries typically report issuances and avoids a million-row table for a million tonnes.
Whatever the representation, three invariants protect against double counting, and an MRV platform should enforce them as database constraints rather than as code reviews:
amount over credits that reference it never exceeds its verifiedAmount.reportId that consumed them, and a second report cannot claim them.The third invariant closes a gap the schemas leave open. Storage events are submitted with status pending-verification (Chapter 4), but nothing in the data model says which report later verified them. Without that back-reference, the same injection could be counted in two overlapping reports.
Once a credit is issued, its status exists in several places: the registry, the operator's MRV platform, possibly a Verifiable Credential and possibly a token on a blockchain. Only one of them can be the ledger of record, and for a registry-issued unit that is the registry. The WIA credit record, the credential and any token are mirrors. They must be updated from the registry, and their consumers should check the registry, or a status source derived from it, before relying on them.
In practice the MRV platform subscribes to registry notifications, or polls where none exist. When a retirement arrives it updates the credit record to retired, records the beneficiary in the retirement extension, updates the credential status (section 6.5) and burns or locks any corresponding token (section 6.6). The reverse flow, in which a token burn or a platform status change is used to tell the registry that a unit is retired, is exactly the arrangement that allows one tonne to be claimed twice.
A registry entry proves that a unit exists and who holds it. It does not, by itself, give a buyer a portable, signed statement of what was verified that can be checked without logging in anywhere. PHASE-4 §6.2 fills that role with a W3C Verifiable Credential (VC) of type CarbonRemovalCertificate, and simulator tab 5 (QR & VC) shows how such a credential might be presented as a QR code.
A VC has an issuer, a subject and a set of claims about the subject, protected by the issuer's signature. For a removal certificate, the natural issuer is the verification body, since it is the party making the verification statement. The operator may issue its own credentials too, but a self-issued removal certificate adds little to an unsigned report.
The claims should be the ones the verifier actually made and nothing that can change later. The simulator's sample credential embeds the credit's registry status ("status": "active"), the technology's efficiency and the operator's specific energy. The status will change the moment the credit is retired, and once signed, a credential cannot be edited. Mutable facts belong in the registry or in a separate status mechanism, and the W3C Bitstring Status List specification provides a standard way to publish revocation or suspension status for credentials. The credential itself should assert the facility, the period, the verified quantity, the storage method, the standards applied, the report identifier and the digest of the evidence package.
Decoded, the credential part of a JWT-encoded removal certificate for the worked example would look like this:
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://mrv.example.com/contexts/dac-removal/v1"
],
"type": ["VerifiableCredential", "CarbonRemovalCertificate"],
"issuer": "did:web:verifier.example.com",
"issuanceDate": "2026-02-20T00:00:00Z",
"credentialSubject": {
"id": "https://mrv.example.com/reports/verify-example-reykjanes-01-2025",
"type": "VerifiedCarbonRemoval",
"facilityId": "dac-example-reykjanes-01",
"period": { "startDate": "2025-01-01", "endDate": "2025-12-31" },
"verifiedAmount": { "value": 846, "unit": "t CO2" },
"uncertaintyPercent": 4.0,
"storageMethod": "mineralization",
"standards": ["ISO 14064-2", "ISO 14064-3"],
"conclusion": "verified",
"evidenceDigest": "sha256:5d8e…",
"registry": { "name": "Puro.earth", "serialBlock": "EXAMPLE-SERIAL-0001 … EXAMPLE-SERIAL-0846" }
}
}
Three decisions in this example differ from the spec's samples. The subject is the verification report, identified by a resolvable URL, rather than a did:wia:capture:… identifier for a single capture event. A report is what was verified, and giving every event its own DID adds key management without adding assurance. The issuer uses did:web, which any verifier can resolve with an ordinary HTTPS request. The spec's did:wia: identifiers can only be used once a did:wia method has been specified and resolvers for it are available to verifiers, and until then a credential issued under them cannot be checked by outsiders. Finally, the custom context URL lives on a domain the issuer controls. The spec's wia.org/credentials/dac/v1 context is illustrative, and a JSON-LD context that does not resolve makes the credential fail processing in stricter verifiers.
The example uses the VC Data Model 1.1 context, as the spec's samples and the JWT library below do. The W3C has since published VC Data Model 2.0, which changes the base context and some property names (for example, validFrom instead of issuanceDate). The spec also mixes securing mechanisms: the simulator shows an embedded Ed25519Signature2020 proof, while the PHASE-4 code produces a JWT. A deployment should choose one data model version and one securing mechanism and record the choice in its profile manifest (Chapter 8).
PHASE-4 §6.2 uses the did-jwt-vc library. Its issuance sample calls a function named createVerifiableCredential with a nested object. The library's documented function for JWT credentials is createVerifiableCredentialJwt, which takes a JWT payload whose vc claim holds the credential. The verification sample has a more serious defect. It imports verifyCredential with const and then declares its own async function verifyCredential in the same scope. JavaScript does not allow a function declaration to redeclare a const binding, so the module fails to load. Had the function been declared in an inner scope instead, it would have shadowed the import and called itself recursively until the stack overflowed.
The following sketch corrects both. It is illustrative. Check it against the library version you install, and keep the private key in a key management system rather than in application memory where possible.
// Illustrative sketch — verify against the installed library versions.
import { createVerifiableCredentialJwt, verifyCredential } from 'did-jwt-vc';
import { EdDSASigner } from 'did-jwt';
import { Resolver } from 'did-resolver';
import { getResolver as getWebResolver } from 'web-did-resolver';
const issuer = {
did: 'did:web:verifier.example.com',
signer: EdDSASigner(loadIssuerKey()), // placeholder: replace with your KMS/HSM key lookup
alg: 'EdDSA',
};
export async function issueRemovalCertificate(report, packageDigest) {
const payload = {
sub: `https://mrv.example.com/reports/${report.reportId}`,
nbf: Math.floor(Date.parse(report.verificationDate) / 1000),
vc: {
'@context': ['https://www.w3.org/2018/credentials/v1',
'https://mrv.example.com/contexts/dac-removal/v1'],
type: ['VerifiableCredential', 'CarbonRemovalCertificate'],
credentialSubject: {
type: 'VerifiedCarbonRemoval',
facilityId: report.facilityId,
period: report.period,
verifiedAmount: { value: report.verifiedAmount, unit: 't CO2' },
conclusion: report.conclusion,
evidenceDigest: packageDigest,
},
},
};
return createVerifiableCredentialJwt(payload, issuer);
}
const resolver = new Resolver({ ...getWebResolver() });
// Named differently from the import, so nothing is redeclared or shadowed.
export async function checkRemovalCertificate(vcJwt) {
try {
const result = await verifyCredential(vcJwt, resolver); // throws if invalid
return { valid: true, subject: result.verifiableCredential.credentialSubject };
} catch (err) {
return { valid: false, reason: String(err.message || err) };
}
}
A valid signature proves only that the named issuer signed these claims. A complete check has three more steps, all of which the spec's QR verification sketch in the simulator gestures at. The verifier must confirm that the issuer DID belongs to an accredited verification body it trusts, that the credential has not been revoked, and, if the credential is being used to support a claim about specific credits, that the registry shows those serials as retired to the claimant.
Simulator tab 5 encodes a JSON object with the certificate ID, facility, amount, date, a verification URL and a credential URL into a QR code. Two refinements make this robust. First, put a single HTTPS URL in the QR code rather than a JSON document. A URL keeps the symbol small, and therefore easier to scan when printed small, and it lets the issuer revoke or update what the link shows without reprinting. The claims themselves should be read from the signed credential behind the link, never from the QR payload, which anyone can forge. Second, keep the symbol dark-on-light with high contrast. The simulator's sample sets red modules on a dark navy background, and that palette makes the lighter colour the "dark" modules, which inverts the symbol. Many scanners fail on inverted or low-contrast QR codes.
FOUNDATION §5.3.2 and PHASE-4 §5.2 propose blockchain integration on public or permissioned networks. They mix two quite different uses, which are worth separating because they have very different risks.
Hash anchoring publishes a digest of data, not the data or a tradable unit. For example, the operator computes a Merkle tree over each day's signed capture summaries and writes the root to a public ledger. Later, anyone holding a summary and its Merkle proof can show that the summary existed, unchanged, at the anchoring time. FOUNDATION §5.3.2 describes exactly this: hashes of verified capture events and Merkle proofs for data integrity. Anchoring creates no asset. If an anchor is lost, only the timestamp evidence is lost; no one's credit disappears.
Tokenization creates a transferable on-chain object that represents a credit, as the spec's ERC-721 contract does. The token is then a second ledger of ownership next to the registry. That is only safe if the registry knows about it, for example by holding the underlying serials in a dedicated account or marking them as represented on chain. If the registry does not know, the same tonne can be retired once in the registry and once on chain, which is the double-counting failure the whole MRV chain exists to prevent.
For most DAC operators the honest question is what a chain adds beyond a transparency log. An append-only log with signed tree heads, following the Certificate Transparency pattern (RFC 9162) or run as a Sigstore Rekor instance, gives the same integrity evidence as hash anchoring without gas fees or wallets. Public-chain anchoring helps when the audience wants independence from any single log operator; tokenization only when a real on-chain market exists that the registry has agreed to support.
The spec's DACCarbonCredit contract is short and readable, and it is not safe to deploy. Table 6-7 lists its defects.
| Defect | Consequence | Fix |
|---|---|---|
mintCredit is external with no access control | Anyone can mint credits for any facility and amount | Restrict minting to an issuer role (e.g. OpenZeppelin AccessControl) held by a key tied to registry-confirmed issuance |
| No uniqueness check on the registry serial | The same registry unit can be minted repeatedly | Keep a mapping of used serial hashes and reject duplicates |
retireCredit sets a flag but does not burn or lock | A "retired" token can still be transferred and resold | Burn on retirement, or block transfers of retired tokens in the transfer hook |
| No event on retirement | Off-chain systems cannot follow retirements reliably | Emit a CreditRetired event with token, serial hash and beneficiary reference |
| Evidence stored as a mutable URL string | The report behind the URL can change after minting | Store the report or package digest (bytes32); the URL is only a hint |
| Free-text amount per token | Amounts can be minted that differ from the registry block | One token per registry unit, or an amount checked against the serial block |
Web3 sample signs with getAccounts() on a public RPC | Public RPC nodes expose no accounts; the call returns nothing to sign with | Use an explicit signer (wallet or KMS-backed) and handle confirmations and reorgs |
A corrected sketch, assuming OpenZeppelin Contracts 5.x and one token per registry unit, looks like this. It is illustrative, has not been audited, and must not be deployed without an independent smart-contract audit and the registry's agreement.
// SPDX-License-Identifier: MIT
// Illustrative only — not audited.
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract DACRemovalUnit is ERC721, AccessControl {
bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
struct Unit {
bytes32 serialHash; // keccak256 of the registry serial number
bytes32 evidenceHash; // digest of the verification report or package
uint16 vintage;
bool retired;
}
mapping(uint256 => Unit) public units;
mapping(bytes32 => bool) public serialUsed;
uint256 public nextTokenId;
event UnitMinted(uint256 indexed tokenId, bytes32 serialHash, bytes32 evidenceHash);
event UnitRetired(uint256 indexed tokenId, bytes32 serialHash, address holder, bytes32 beneficiaryRef);
constructor(address admin) ERC721("DAC Removal Unit (example)", "DACRU") {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function mintUnit(address to, bytes32 serialHash, bytes32 evidenceHash, uint16 vintage)
external onlyRole(ISSUER_ROLE) returns (uint256 tokenId)
{
require(!serialUsed[serialHash], "serial already tokenized");
serialUsed[serialHash] = true;
tokenId = nextTokenId++;
units[tokenId] = Unit(serialHash, evidenceHash, vintage, false);
_safeMint(to, tokenId);
emit UnitMinted(tokenId, serialHash, evidenceHash);
}
function retireUnit(uint256 tokenId, bytes32 beneficiaryRef) external {
require(ownerOf(tokenId) == msg.sender, "not holder");
units[tokenId].retired = true;
_burn(tokenId); // a retired unit can no longer move
emit UnitRetired(tokenId, units[tokenId].serialHash, msg.sender, beneficiaryRef);
}
function supportsInterface(bytes4 id)
public view override(ERC721, AccessControl) returns (bool)
{
return super.supportsInterface(id);
}
}
Two details deserve a comment. The beneficiary is passed as a bytes32 reference rather than a name, because anything written to a public chain is public and permanent, and a beneficiary may be a person whose data falls under privacy law such as the GDPR. And burning in retireUnit makes a second retirement impossible, because ownerOf reverts for a burned token. The units mapping keeps the history readable after the burn.
Even the corrected contract only moves the question. It is safe if the ISSUER_ROLE key mints a token only after the registry has confirmed the corresponding serial is held for on-chain representation, and if a retirement event is reported to the registry, which remains the ledger of record, before anyone relies on it. Without that link to the registry the contract is a well-written second ledger, and the double-counting risk remains.
The chain is now complete: measured tonnes, a verified and conservatively bounded quantity, registry units with a single ledger of record, and portable signed evidence. Chapter 7 moves from the credit chain to the systems around the plant: pipelines and storage sites, energy and grid coordination, enterprise carbon accounting and open data.