📦 Contents

Chapter 2: Data Format I — Facility, Operational and Sorbent Records

Chapter 1 argued that a direct air capture (DAC) claim is only as good as the data behind it: a tonne of carbon dioxide removal (CDR) cannot be seen, weighed or inspected after it has gone underground, so the only thing a buyer, verifier or registry ever receives is a chain of records. This chapter begins building that chain from the bottom. It covers the three record types that describe the plant itself and what it does minute by minute: the Facility Information record, the real-time capture (operational) record and the Sorbent Material record. Chapter 3 continues with the records that describe what happens to the captured CO2 afterwards — storage events, verification reports and carbon removal credits.

The source for everything here is PHASE-2-DATA of the WIA Direct Air Capture standard (spec identifier WIA-ENE-050, slug direct-air-capture), together with the measurement requirements in PHASE-1-FOUNDATION §5.1. The schemas are written in JSON Schema draft 2020-12. Rather than reprinting them in full, this chapter walks through the fields that carry meaning, points out where the spec is ambiguous, and adds the checks a production validator needs beyond what JSON Schema can express. All example values in this chapter belong to a fictional plant, dac-example-reykjanes-01, operated by a fictional company; they are illustrative and are not measurements from any real facility.

2.1 Seven Data Categories and the Design Principles Behind Them

PHASE-2-DATA §1.2 divides DAC data into seven categories. It helps to read them as answers to seven different questions an auditor might ask:

# Category Question it answers Where it lives in the spec Covered in
1Facility dataWhat plant is this, and what is it designed to do?Facility Information schema (§2)This chapter, 2.2
2Operational dataWhat is the plant doing right now?Real-Time Capture schema (§3)This chapter, 2.3
3Sorbent dataWhat material does the capturing, and how worn is it?Sorbent Material schema (§4)This chapter, 2.4
4Energy dataHow much energy was used, and from what source?No stand-alone schema: the energy block of the capture record plus energySource in the facility record2.2, 2.3; grid coordination in Ch.7
5Storage dataWhere did the CO2 go?Storage Event schema (§5)Ch.3
6Verification dataWho checked the numbers, and what did they find?Verification Report schema (§6)Ch.3, Ch.6
7Carbon credit dataWhat was sold, to whom, and has it been retired?Carbon Credit schema (§7)Ch.3, Ch.6

Note the fourth row. Although the spec lists energy as its own category, it does not define a separate energy schema. Energy information is split between the design-time figure energySource.energyPerTonCO2 in the facility record and the measured figures in the energy block of every capture record. This is workable, but it means an implementer who wants hourly energy provenance (for example, which share came from a specific power purchase agreement) has to carry it in an extension until the standard adds a dedicated record. Chapter 7 returns to this when it discusses grid coordination.

The spec lists five design principles for all seven categories. In practice they become engineering decisions:

2.2 The Facility Information Record

The facility record is the anchor every other record points to. It changes rarely — when a module is added, a storage partner changes or the plant goes into maintenance — and each change should create a new version rather than overwrite the old one, because verification reports issued in the past must still resolve to the facility description that was valid at the time.

Identity and required fields

Five top-level properties are required: facilityId, name, location, technology and capacity. Inside those objects, five more fields are required: location.latitude, location.longitude, location.country, technology.type and capacity.annualTonsCO2. Everything else is optional, which means a minimal valid facility record is quite small. A registry that wants more — say, a storage partner and an energy source before it will list a project — should express that as a profile on top of the base schema, not by editing the base schema.

The identifier must match ^dac-[a-z0-9-]+$: the literal prefix dac-, then lowercase letters, digits and hyphens. Uppercase letters, underscores and dots are rejected. The pattern does not impose any internal structure, so it is worth agreeing on one inside an organisation. A convenient convention is dac-<operator>-<site>-<nn>. Because the ID is referenced by every capture record, storage event and credit, it must never be reused, even after a facility is decommissioned.

location.country uses ISO 3166-1 alpha-2 codes (two uppercase letters, such as IS, US or KR). The schema's pattern ^[A-Z]{2}$ only checks the shape, so XX passes; a strict validator should check against the actual code list. Latitude and longitude are bounded to ±90 and ±180 degrees. The schema does not name a datum; WGS 84 is the de facto choice and should be stated in the implementation's profile. altitude is in metres above sea level.

Enumerations

Four enumerations in the facility record carry most of its meaning:

Field Allowed values Implementation note
technology.typesolid-sorbent-dac, liquid-solvent-dac, electrochemical-dac, hybrid-dacDrives which sub-blocks are meaningful. A liquid-solvent plant has no per-module sorbent saturation; the sorbent block of its capture records should be omitted rather than filled with zeros.
operational.statusoperational, testing, maintenance, decommissionedA facility-level lifecycle state. Do not confuse it with status.operationalState in the capture record, which describes the current cycle phase.
storage.permanencepermanent, >10000-years, >1000-years, variesThese are labels defined by the standard, not scientific findings. The durability actually credited is determined by the storage pathway and the registry methodology (Ch.3, Ch.6).
operational.maintenanceCycleweekly, monthly, quarterly, annuallyThe planned preventive maintenance cadence. It sets expectations for how often capture records will show maintenance and helps explain uptime.

Two further enumerations are worth knowing. energySource.type takes one of geothermal, solar, wind, hydro, nuclear, natural-gas or grid-mix, and storage.method takes one of mineralization, geological-storage, ocean-storage, utilization or hybrid. Because each is a single value, a plant that runs on geothermal heat plus grid electricity cannot say so precisely. In that case, use the value that best describes the dominant energy source, set energySource.renewable honestly, and put the breakdown in an extension object. The field that matters for net removal is the measured energy in the capture records, not this label.

Capacity: nameplate, not performance

The capacity block records design capacity: annualTonsCO2 (tonnes per year), modules and captureRatePerModule (tonnes of CO2 per module per day). The three values are related by simple arithmetic, and a facility record in which they disagree should be rejected:

annualTonsCO2 ≈ modules × captureRatePerModule × 365

This is the nameplate figure at 100 % availability. Actual annual capture will be lower, because operational.uptime is below 1 and because capture rate varies with ambient conditions. Keep nameplate and measured values apart: the facility record says what the plant was built to do, while the sum of capture records says what it did. A registry comparing the two can spot a facility that consistently claims more than its nameplate — a classic sign of a unit error or double reporting.

energySource.energyPerTonCO2 is also a design value, in MWh per tonne of CO2. The measured counterpart is energy.specificEnergy in the capture record. They share a unit but not a meaning, and dashboards should never mix them in the same series.

An annotated example

The following record describes the fictional plant used throughout this book. Its numbers are chosen to be internally consistent (12 modules × 0.25 t/day × 365 = 1,095 t/yr); they are examples, not typical industry values.

{
  "facilityId": "dac-example-reykjanes-01",
  "name": "Example Reykjanes DAC Plant 1 (fictional)",
  "location": {
    "latitude": 63.85,
    "longitude": -22.60,
    "altitude": 40,
    "country": "IS",
    "region": "Example region"
  },
  "technology": {
    "type": "solid-sorbent-dac",
    "sorbent": "sorbent-example-amine-b07",
    "manufacturer": "Example Capture Systems",
    "model": "EX-M250",
    "generation": "pilot-2"
  },
  "capacity": {
    "annualTonsCO2": 1095,
    "modules": 12,
    "captureRatePerModule": 0.25
  },
  "energySource": {
    "type": "geothermal",
    "provider": "Example Heat & Power",
    "renewable": true,
    "energyPerTonCO2": 2.0
  },
  "storage": {
    "method": "mineralization",
    "site": "site-example-basalt-01",
    "permanence": "permanent"
  },
  "operational": {
    "startDate": "2024-01-10",
    "status": "operational",
    "uptime": 0.95,
    "maintenanceCycle": "monthly"
  },
  "contacts": {
    "operator": "Example Carbon Ltd.",
    "email": "ops@example.com",
    "website": "https://example.com"
  },
  "metadata": {
    "createdAt": "2024-01-05T00:00:00Z",
    "updatedAt": "2026-03-01T00:00:00Z",
    "version": "1.0.0"
  }
}

A few details are worth pointing out. First, technology.sorbent is a free-text string in the schema, but here it carries the sorbentId of a Sorbent Material record (section 2.4). That turns a description into a link, which is the linked-data principle in action. Second, storage.depth (omitted here) is also free text ("Storage depth range"). A string such as "700–2000 m" cannot be queried or checked. If depth matters to your registry, add a structured extension such as {"minM": …, "maxM": …} and keep the string for display only. Third, metadata.version is the version of this record, not of the schema; the schema version belongs in $schema or in a profile identifier.

2.3 The Real-Time Capture Record

If the facility record is the anchor, the capture record is the heartbeat. A plant emits one every sampling interval — PHASE-1-FOUNDATION §5.1.1 asks for every 1–5 minutes — so a single facility produces hundreds of thousands of them per year. Everything later in the chain (hourly aggregates, daily verification summaries, storage custody transfers and finally credits) is computed from these records, which is why their consistency matters so much.

Required fields and blocks

Four properties are required: timestamp (RFC 3339 / ISO 8601 date-time), facilityId (same pattern as above), capture and energy. Within capture, only currentRate is required. The record is organised into six blocks:

Ambient conditions belong in the record because capture performance depends on them. Solid amine sorbents, for example, are sensitive to humidity and temperature, and ambient CO2 concentration sets how much CO2 is available per cubic metre of air processed. When a verifier later sees a dip in capture rate, the environmental block is what lets the operator show that it was caused by weather, not by a fault or by data manipulation.

Operational state and alerts

status.operationalState takes one of five values: adsorption, regeneration, idle, maintenance or error. For a solid sorbent plant, the first two alternate in a cycle (Chapter 5 formalises this as a state machine). For a continuously operating liquid-solvent plant, the distinction is less natural, since air contact and regeneration happen at the same time in different units; such a plant typically reports adsorption while the air contactor is running and uses the facility-level maintenance status for outages.

A key question is whose state this is. In a multi-module solid-sorbent plant, some modules are adsorbing while others regenerate, so a single facility-level state is ambiguous. The schema has no module identifier. The recommended practice is either to emit one record per module (and add a module ID in an extension) or to emit one record per facility with operationalState describing the majority of modules and the per-module breakdown in an extension. Whichever you choose, write it in your profile, because it changes how totals are summed.

Each entry in alerts has a severity (info, warning, error or critical), a message and its own timestamp. Alerts are part of the audit trail: a verifier who sees a gap in capture data will look for a matching alert. Messages should be stable, machine-parsable codes followed by human text (for example, "HUMIDITY_HIGH: inlet RH above configured limit") rather than free prose only.

An annotated example

The record below was taken, in our fictional scenario, six hours into a UTC day. Its values are illustrative and deliberately consistent with the facility record above and with each other; section 2.7 checks them.

{
  "timestamp": "2026-03-15T06:00:00Z",
  "facilityId": "dac-example-reykjanes-01",
  "capture": {
    "currentRate": 2.9,
    "dailyTotal": 0.72,
    "efficiency": 0.78,
    "co2Purity": 97.2
  },
  "energy": {
    "currentConsumption": 242,
    "dailyTotal": 1.45,
    "specificEnergy": 2.01,
    "renewablePercentage": 100
  },
  "environmental": {
    "ambientCO2": 425,
    "temperature": 4.0,
    "humidity": 78,
    "pressure": 1008,
    "windSpeed": 6.2
  },
  "sorbent": {
    "cycleNumber": 3120,
    "saturation": 0.62,
    "expectedLifetime": 11157,
    "regenerationTemp": 98
  },
  "airflow": {
    "rate": 4200,
    "pressure": 100800,
    "pressureDrop": 180
  },
  "status": {
    "operationalState": "adsorption",
    "alerts": [
      {
        "severity": "info",
        "message": "HUMIDITY_HIGH: inlet RH above advisory level",
        "timestamp": "2026-03-15T05:52:10Z"
      }
    ]
  }
}

Here currentRate is expressed at facility level (2.9 t/day across all 12 modules, slightly below the nameplate 3.0 t/day). The spec's field description ("Current capture rate in tons/day") does not say whether the rate is per module or per facility. The difference is a factor equal to the number of modules, which is exactly the kind of silent error that later breaks a mass balance. Pick one meaning, record it in the profile, and check it (2.7). Note also that monthlyTotal and yearlyTotal are omitted: running totals over long periods are better computed server-side from the stored records than trusted from an edge device.

2.4 The Sorbent Material Record

For solid-sorbent plants, the sorbent is both the capture engine and the main consumable. Its condition determines how much CO2 each cycle can carry, how much heat is needed per tonne and when a replacement (with its own embodied emissions) is due. The Sorbent Material record keeps that history in one place.

Identity, type and properties

Three fields are required: sorbentId, which must match ^sorbent-[a-z0-9-]+$; type; and the properties object. type is one of solid-amine, liquid-solvent, metal-organic-framework, zeolite, activated-carbon or other. The identifier should identify a batch installed at a site, not a product line, because degradation is tracked per installed batch; the separate batchNumber field holds the manufacturer's lot number.

The properties object describes the material as specified or tested:

The lifecycle object tracks the installed batch over time: currentCycles, currentCapacity (mmol/g), degradation (a cumulative fraction from 0 to 1) and remainingLifetime (cycles). Together with installDate and replacementDate, this is what lets an auditor tie a drop in plant output to sorbent ageing rather than to an unexplained loss.

The spec's sample sorbent record uses specific numbers for loading, lifetime and selectivity. Treat them as placeholders. They are not a published performance benchmark, and real values depend heavily on the material, the process conditions and the test method. Report your own tested values and cite the test method in an extension.

Worked calculation: capacity after N cycles

The schema gives a per-cycle degradation rate but does not say how to apply it. There are two reasonable models, and they give slightly different answers:

Geometric (compounding):  C(N) = C0 × (1 − r)^N
Linear:                   C(N) = C0 × (1 − r × N)

C0 = initial capacity (mmol/g), r = degradationRate, N = cycles

Take an illustrative batch with a workingCapacity of 1.2 mmol/g and an assumed degradationRate of 2 × 10⁻⁵ per cycle, after 3,120 cycles:

Quantity Geometric model Linear model
Retained fraction after 3,120 cycles(1 − 0.00002)^3120 ≈ 0.93951 − 0.0624 = 0.9376
currentCapacity (mmol/g)1.2 × 0.9395 ≈ 1.1271.2 × 0.9376 ≈ 1.125
degradation (cumulative)≈ 0.06050.0624
Cycles until 80 % of initial capacityln 0.8 / ln(1 − 0.00002) ≈ 11,1570.2 / 0.00002 = 10,000
remainingLifetime at N = 3,120≈ 8,0376,880

Over a few thousand cycles the two models barely differ, but the end-of-life prediction differs by more than a thousand cycles. The 80 % end-of-life threshold here is itself only an example; the standard does not define one. A conforming implementation should therefore write down three things in its profile: which model it uses, which capacity the rate applies to (maxLoading or workingCapacity), and which threshold defines end of life. Otherwise, two systems reading the same record will compute different remainingLifetime values. Note that the capture record's sorbent.expectedLifetime (11,157 cycles in the example in 2.3) should come from the same calculation as the sorbent record, not from a separate constant.

It is also useful to convert capacity into mass of CO2, which links the sorbent record to the capture record. The molar mass of CO2 is about 44.01 g/mol, so 1 mmol/g corresponds to about 44 mg of CO2 per gram of sorbent. At 1.127 mmol/g, each full working cycle moves about 49.6 mg CO2 per gram, or roughly 0.050 t CO2 per tonne of sorbent. If, for the sake of example, a module completed two full cycles a day (the specification's example adsorption phase alone lasts 8–12 hours, see Chapter 5), capturing 0.25 t/day would need about 0.25 ÷ (2 × 0.0496) ≈ 2.5 t of sorbent in that module. The number of cycles per day is an assumption here, not a spec value, but the method gives a verifier a quick plausibility check on reported sorbent inventories.

2.5 Units and Scales: Where Implementations Go Wrong

The schemas state units only in description strings, which validators ignore. Worse, the same kind of quantity uses different scales in different places. The following table collects the fields that most often cause errors in the three record types covered in this chapter.

Field Unit / scale in the spec Suggested UCUM code Pitfall
capture.efficiencyfraction 0–11Sending 78 instead of 0.78 fails validation (maximum 1) — good.
capture.co2Puritypercentage 0–100%Sending 0.972 instead of 97.2 passes validation and means 0.972 % purity.
operational.uptimefraction 0–1 (though described as a "percentage")1The description and the range disagree; the range wins.
sorbent.saturation, lifecycle.degradationfraction 0–11Regeneration triggers (Ch.5) are expressed against this fraction.
energy.renewablePercentage, environmental.humiditypercentage 0–100%Same silent-pass risk as co2Purity.
environmental.pressurehPahPaAround 1,000 at sea level.
airflow.pressure, airflow.pressureDropPaPaAround 100,000 for absolute pressure; a factor of 100 from the field above.
properties.regenerationPressurebarbarAbsolute or gauge is not stated; vacuum regeneration makes this important.
capture.currentRate, captureRatePerModulet/dayt/dPer module or per facility must be fixed in the profile (2.3).
capture.dailyTotalt (cumulative since 00:00 UTC)t"Day" must be UTC, not local plant time.
energy.currentConsumptionkWkWPower, not energy; do not sum it.
energy.dailyTotalMWhMW.hShares a name with capture.dailyTotal but not a unit.
energy.specificEnergy, energyPerTonCO2MWh/tMW.h/tElectricity and heat are lumped together; see below.
environmental.ambientCO2ppm[ppm]Mole fraction (µmol/mol) should be stated; dry or wet air matters.
maxLoading, workingCapacity, currentCapacitymmol/gmmol/gSome datasheets use wt % or g/kg; convert before recording.
airflow.ratem³/minm3/minActual or standard (normalised) volume is not stated.

Two structural gaps are hidden in this table. The first is fractions versus percentages. A percentage field that receives a fraction still passes validation, because a value such as 0.972 is inside the 0–100 range. The reverse error is caught, but this one is not. The second is heat versus electricity. For a solid-sorbent plant, most of the energy demand is low-temperature heat for regeneration, while fans and vacuum pumps use electricity. A single MWh/t figure hides which is which, yet the two usually have very different emission factors. Net removal (Chapter 1, section 1.4) needs them separately.

The standard does not define a unit annotation mechanism, so this book recommends one that is compatible with it. At schema level, add a custom keyword such as "x-unit": "MW.h/t" next to each numeric property, using UCUM (the Unified Code for Units of Measure) codes; JSON Schema 2020-12 permits unknown keywords as annotations. At record level, add an extension object that splits energy by carrier. For example:

"energy": {
  "currentConsumption": 242,
  "dailyTotal": 1.45,
  "specificEnergy": 2.01,
  "renewablePercentage": 100,
  "x-breakdown": {
    "electricityMWh": 0.38,
    "heatMWh": 1.07,
    "heatSource": "geothermal",
    "unitsNote": "MWh, 00:00Z to timestamp; illustrative values"
  }
}

Because the base schemas allow additional properties, this extension validates against the standard schema unchanged, and consumers that do not know it simply ignore it. The rule for any extension is that it must never contradict a base field: here electricityMWh + heatMWh must equal dailyTotal.

2.6 Measurement Requirements Attached to the Fields

A schema says what a valid record looks like; PHASE-1-FOUNDATION §5.1 says how the numbers in it must be produced and kept. These requirements are what turn a JSON document into evidence.

Temporal resolution

The standard defines four cadences: operational data collected every 1–5 minutes, aggregated data reported hourly, daily summaries for verification and monthly aggregates for public reporting. In terms of the records in this chapter, the capture record is the 1–5-minute product. Hourly and daily products are computed from it, and each computed value should state which raw records it covers (start and end timestamps plus record count), so that a verifier can recompute it. Missing intervals should be visible as gaps, not filled by interpolation, unless the fill is flagged; the QC flags that carry this are described in Chapter 5.

Accuracy

The standard sets an accuracy target and a confidence level for each measured quantity:

Parameter Accuracy Confidence Record field it applies to
CO2 captured±5 %95 %capture.currentRate, capture.dailyTotal
Energy consumption±3 %95 %energy.dailyTotal, energy.currentConsumption
Sorbent mass±2 %99 %Sorbent inventory (extension; not a base field)
Ambient CO2±10 ppm90 %environmental.ambientCO2
Temperature±1 °C95 %environmental.temperature, sorbent.regenerationTemp
Pressure±5 %95 %environmental.pressure, airflow.*

The schemas have no field to carry the uncertainty of an individual value. Since Chapter 6 will propagate these uncertainties into the verified amount, it is good practice to record, per facility and per instrument, the calibration certificate and its stated uncertainty, and to reference that calibration record from the data. A capture value without a traceable instrument behind it cannot be defended at the ±5 % level, however many decimal places it has. Note also that the confidence levels differ from row to row (90 %, 95 %, 99 %), so the figures cannot simply be combined without first bringing them to a common basis.

Retention

The standard sets minimum retention periods: raw data for 7 years, aggregated data for 30 years, and verification records and blockchain hashes permanently. For capture records at a 1-minute interval, 7 years of raw data from a single facility is several million records — easily managed by a time-series database, but only if storage is planned from the start. Two practical consequences follow. First, aggregates that outlive their raw data must be self-describing: they must carry the method, the count of source records and a hash of the source set, because after year 7 nobody can recompute them. Second, where a jurisdiction or registry demands longer retention, the longer period applies; the standard's figures are a floor, not a ceiling.

2.7 Consistency Checks Beyond JSON Schema

JSON Schema validates each field on its own. It can say that dailyTotal is a non-negative number, but not that it is plausible given currentRate, or that specificEnergy really equals energy divided by CO2. Those cross-field relations are where real data errors show up, and the spec's own example records fail several of them — a strong reason why this book uses its own example. A conforming validator should run at least the checks below after schema validation passes.

Check Relation Example values (2.2, 2.3)
C1 NameplateannualTonsCO2 ≈ modules × captureRatePerModule × 36512 × 0.25 × 365 = 1,095 ✓
C2 Rate ceilingcurrentRate ≤ modules × captureRatePerModule × (1 + tolerance)2.9 ≤ 3.0 ✓
C3 Daily totaldailyTotal ≤ max rate × hours since 00:00Z ÷ 240.72 ≤ 3.0 × 6 ÷ 24 = 0.75 ✓
C4 Specific energyspecificEnergy ≈ energy.dailyTotal ÷ capture.dailyTotal1.45 ÷ 0.72 ≈ 2.01 ✓
C5 Power vs. energyenergy.dailyTotal ≈ average kW × hours ÷ 1000242 × 6 ÷ 1000 ≈ 1.45 ✓
C6 Pressure scalesairflow.pressure (Pa) near environmental.pressure (hPa) × 100 for an open contactor100,800 ≈ 1,008 × 100 ✓
C7 ReferencesfacilityId exists; technology.sorbent resolves to a sorbent recordboth resolve ✓
C8 Unknown fieldsevery property is in the schema or starts with x-only x-breakdown ✓

C3 uses the maximum rate rather than currentRate because the rate varies through the day; a daily total that exceeds what the plant could have captured at full nameplate since midnight is impossible, whatever the current rate is. C5 is only approximate, because currentConsumption is an instantaneous reading and the proper check integrates it over the stored records. C6 applies only where the contactor operates near ambient pressure; during vacuum regeneration, the system pressure is expected to be far lower. The tolerances are implementation choices: a sensible starting point is to use the accuracy targets from 2.6 (for example, 5 % for CO2 and 3 % for energy), and to treat a failure as a warning that must be explained rather than as an automatic rejection.

Implementation with Ajv

The following Node.js sketch validates a capture record against the 2020-12 schema with Ajv, then runs the cross-field checks. The schema file names and the profile object are illustrative; the Ajv API calls are the library's real ones.

// npm install ajv ajv-formats
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { readFileSync } from "node:fs";

const load = (p) => JSON.parse(readFileSync(p, "utf8"));

const ajv = new Ajv2020({ allErrors: true, strict: false }); // strict:false lets "x-unit" pass
addFormats(ajv);                                             // date, date-time, email, uri
const validateCapture = ajv.compile(load("schemas/dac-operational.schema.json"));

// Profile decisions from 2.3–2.5, written down once
const profile = { rateScope: "facility", tolCO2: 0.05, tolEnergy: 0.03 };

function crossChecks(rec, facility) {
  const issues = [];
  const cap = facility.capacity;
  const nameplateDay = cap.modules * cap.captureRatePerModule;          // t/day

  // C1 nameplate consistency (facility record)
  const annual = nameplateDay * 365;
  if (Math.abs(annual - cap.annualTonsCO2) > profile.tolCO2 * cap.annualTonsCO2)
    issues.push(`C1: annualTonsCO2 ${cap.annualTonsCO2} vs computed ${annual}`);

  // C2 rate ceiling
  if (rec.capture.currentRate > nameplateDay * (1 + profile.tolCO2))
    issues.push("C2: currentRate above nameplate (per-module value sent?)");

  // C3 daily total against elapsed UTC time
  const t = new Date(rec.timestamp);
  const hours = t.getUTCHours() + t.getUTCMinutes() / 60;
  const d = rec.capture.dailyTotal;
  if (d !== undefined && d > nameplateDay * hours / 24 * (1 + profile.tolCO2))
    issues.push(`C3: dailyTotal ${d} t impossible after ${hours.toFixed(2)} h`);

  // C4 specific energy
  const e = rec.energy;
  if (d > 0 && e.dailyTotal !== undefined && e.specificEnergy !== undefined) {
    const se = e.dailyTotal / d;
    if (Math.abs(se - e.specificEnergy) > profile.tolEnergy * se)
      issues.push(`C4: specificEnergy ${e.specificEnergy} vs computed ${se.toFixed(2)}`);
  }
  return issues;
}

const facility = load("data/dac-example-reykjanes-01.json");
const record = load("data/capture-2026-03-15T0600Z.json");

if (!validateCapture(record)) {
  console.error(validateCapture.errors);          // schema errors: reject
} else {
  const warnings = crossChecks(record, facility); // cross-field: explain or reject
  console.log(warnings.length ? warnings : "OK");
}

Three design points are worth noting. First, schema errors and cross-field warnings are reported separately: the first mean the record is malformed, the second mean it is well formed but physically doubtful. Second, the C2 message suggests the most common cause. When a rate comes in far above nameplate, the usual explanation is that a per-facility field received a per-module value or the other way round, as described in 2.3. Third, the profile object makes the implementation's interpretations explicit. When the same object is published with the conformance claim (Chapter 8), a verifier can see exactly which rules were applied.

The unknown-field check (C8) is best done by generating a strict copy of the schema with unevaluatedProperties: false on every object and a patternProperties entry that allows names beginning with x-. Run it in warning mode alongside the permissive standard schema. That way, typos such as captureRatePerModul are surfaced without rejecting legitimate extensions.

Chapter Summary

The facility, capture and sorbent records form the physical base of the evidence chain. The facility record fixes identity and design capacity, the capture record records what happened at 1–5-minute resolution, and the sorbent record explains why capacity changes over time. The standard's schemas get the structure right but leave four things to the implementer: the scale of each number (fraction or percentage, Pa or hPa), the scope of each rate (per module or per facility), the model behind derived lifetimes, and the cross-field relations that make a record physically plausible. A conforming implementation settles each of these in a published profile and checks them automatically. Chapter 3 follows the captured CO2 out of the plant, through storage events and verification reports to carbon removal credits.