diff --git a/embargoed-data-license-guard/README.md b/embargoed-data-license-guard/README.md
new file mode 100644
index 00000000..4f36fbbe
--- /dev/null
+++ b/embargoed-data-license-guard/README.md
@@ -0,0 +1,49 @@
+# Embargoed Data License Guard
+
+This module is a focused Revenue Infrastructure slice for issue #20. It checks
+whether an anonymized research metadata or data-licensing export can be released
+to an institutional, government, or market-intelligence customer.
+
+It is intentionally self-contained:
+
+- no payment processor calls
+- no external APIs
+- no credentials
+- no private research content
+- synthetic packets only
+
+## What It Checks
+
+- Embargo windows have ended or the license has a valid early-access grant.
+- Exclusive licenses are not released to the wrong customer.
+- Cohort size is large enough for anonymized export.
+- Consent coverage meets the license policy.
+- License tier permits the requested export type.
+- Required attribution, redaction, and data-processing terms are present.
+- Deliverables are not stale relative to the license snapshot.
+
+## Run
+
+```bash
+npm test
+npm run demo
+```
+
+The demo writes reviewer artifacts under `artifacts/`:
+
+- `demo-output.json`
+- `demo-report.md`
+- `release-summary.svg`
+
+## Requirement Mapping
+
+Issue #20 calls out monetizing structured, anonymized usage and research
+metadata through licensing APIs and analytics. This guard focuses on the release
+control before such a licensing export is delivered or invoiced.
+
+| Issue requirement | Covered by |
+| --- | --- |
+| Data licensing models for institutional and government customers | customer/license packet fields |
+| Anonymized metadata, never private content | cohort and redaction checks |
+| API/dashboard licensing options | `exportType` tier entitlement checks |
+| Predictable revenue and compliance-safe delivery | deterministic release decisions |
diff --git a/embargoed-data-license-guard/artifacts/demo-output.json b/embargoed-data-license-guard/artifacts/demo-output.json
new file mode 100644
index 00000000..579d3df7
--- /dev/null
+++ b/embargoed-data-license-guard/artifacts/demo-output.json
@@ -0,0 +1,68 @@
+[
+ {
+ "licenseId": "lic-gov-neuro-2026",
+ "customer": "NIH Policy Lab",
+ "exportType": "api",
+ "decision": "RELEASE",
+ "findings": [],
+ "financeAction": "release_export_and_invoice"
+ },
+ {
+ "licenseId": "lic-market-rare-disease-2026",
+ "customer": "Market Intel A",
+ "exportType": "periodic_snapshot",
+ "decision": "REVIEW",
+ "findings": [
+ {
+ "code": "EARLY_ACCESS_REVIEW",
+ "severity": "REVIEW",
+ "message": "Export is still under embargo but has an early-access approval."
+ },
+ {
+ "code": "CONSENT_GAP",
+ "severity": "REVIEW",
+ "message": "Consent coverage 92% is below required 95%."
+ }
+ ],
+ "financeAction": "route_to_licensing_operations"
+ },
+ {
+ "licenseId": "lic-exclusive-funder-2026",
+ "customer": "Regional Consortium",
+ "exportType": "white_label",
+ "decision": "HOLD",
+ "findings": [
+ {
+ "code": "EXCLUSIVITY_CONFLICT",
+ "severity": "HOLD",
+ "message": "License conflicts with an active exclusive customer grant."
+ },
+ {
+ "code": "COHORT_TOO_SMALL",
+ "severity": "HOLD",
+ "message": "Cohort size 42 is below minimum 100."
+ },
+ {
+ "code": "CONSENT_GAP",
+ "severity": "REVIEW",
+ "message": "Consent coverage 88% is below required 95%."
+ },
+ {
+ "code": "TIER_NOT_ENTITLED",
+ "severity": "HOLD",
+ "message": "Dashboard license does not permit white_label exports."
+ },
+ {
+ "code": "MISSING_RELEASE_TERMS",
+ "severity": "REVIEW",
+ "message": "Missing data processing agreement, redaction profile, attribution text."
+ },
+ {
+ "code": "STALE_SNAPSHOT",
+ "severity": "REVIEW",
+ "message": "Snapshot is 57 days old; maximum is 30."
+ }
+ ],
+ "financeAction": "hold_export_and_block_invoice"
+ }
+]
diff --git a/embargoed-data-license-guard/artifacts/demo-report.md b/embargoed-data-license-guard/artifacts/demo-report.md
new file mode 100644
index 00000000..e59e4a4a
--- /dev/null
+++ b/embargoed-data-license-guard/artifacts/demo-report.md
@@ -0,0 +1,25 @@
+# Embargoed Data License Guard Demo
+
+| License | Customer | Export | Decision | Finance action |
+| --- | --- | --- | --- | --- |
+| lic-gov-neuro-2026 | NIH Policy Lab | api | RELEASE | release_export_and_invoice |
+| lic-market-rare-disease-2026 | Market Intel A | periodic_snapshot | REVIEW | route_to_licensing_operations |
+| lic-exclusive-funder-2026 | Regional Consortium | white_label | HOLD | hold_export_and_block_invoice |
+
+### lic-gov-neuro-2026
+
+ - RELEASE: no blocking findings
+
+### lic-market-rare-disease-2026
+
+ - REVIEW: EARLY_ACCESS_REVIEW - Export is still under embargo but has an early-access approval.
+ - REVIEW: CONSENT_GAP - Consent coverage 92% is below required 95%.
+
+### lic-exclusive-funder-2026
+
+ - HOLD: EXCLUSIVITY_CONFLICT - License conflicts with an active exclusive customer grant.
+ - HOLD: COHORT_TOO_SMALL - Cohort size 42 is below minimum 100.
+ - REVIEW: CONSENT_GAP - Consent coverage 88% is below required 95%.
+ - HOLD: TIER_NOT_ENTITLED - Dashboard license does not permit white_label exports.
+ - REVIEW: MISSING_RELEASE_TERMS - Missing data processing agreement, redaction profile, attribution text.
+ - REVIEW: STALE_SNAPSHOT - Snapshot is 57 days old; maximum is 30.
diff --git a/embargoed-data-license-guard/artifacts/release-summary.svg b/embargoed-data-license-guard/artifacts/release-summary.svg
new file mode 100644
index 00000000..c7ff21c5
--- /dev/null
+++ b/embargoed-data-license-guard/artifacts/release-summary.svg
@@ -0,0 +1,6 @@
+
diff --git a/embargoed-data-license-guard/package.json b/embargoed-data-license-guard/package.json
new file mode 100644
index 00000000..66ad2c03
--- /dev/null
+++ b/embargoed-data-license-guard/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "embargoed-data-license-guard",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Dependency-free release guard for embargoed research data licensing exports.",
+ "type": "module",
+ "scripts": {
+ "demo": "node src/cli.js",
+ "test": "node test/license-guard.test.js"
+ }
+}
diff --git a/embargoed-data-license-guard/src/cli.js b/embargoed-data-license-guard/src/cli.js
new file mode 100644
index 00000000..5d872595
--- /dev/null
+++ b/embargoed-data-license-guard/src/cli.js
@@ -0,0 +1,17 @@
+import { evaluateLicenseRelease } from "./licenseGuard.js";
+import { writeArtifacts } from "./report.js";
+import { samplePackets } from "./samplePackets.js";
+
+const results = samplePackets.map((packet) => evaluateLicenseRelease(packet));
+
+for (const result of results) {
+ console.log(
+ `${result.licenseId}: ${result.decision} (${result.financeAction})`,
+ );
+ for (const finding of result.findings) {
+ console.log(` - ${finding.severity} ${finding.code}: ${finding.message}`);
+ }
+}
+
+writeArtifacts(results);
+console.log("Artifacts written to artifacts/");
diff --git a/embargoed-data-license-guard/src/licenseGuard.js b/embargoed-data-license-guard/src/licenseGuard.js
new file mode 100644
index 00000000..e3c94db5
--- /dev/null
+++ b/embargoed-data-license-guard/src/licenseGuard.js
@@ -0,0 +1,164 @@
+const EXPORT_TIER_ORDER = {
+ dashboard: 1,
+ periodic_snapshot: 2,
+ api: 3,
+ white_label: 4,
+};
+
+const DECISION_PRIORITY = {
+ RELEASE: 0,
+ REVIEW: 1,
+ HOLD: 2,
+};
+
+export function evaluateLicenseRelease(packet, today = "2026-06-27") {
+ const findings = [
+ checkEmbargo(packet, today),
+ checkExclusivity(packet),
+ checkCohortSize(packet),
+ checkConsentCoverage(packet),
+ checkTier(packet),
+ checkTerms(packet),
+ checkSnapshotFreshness(packet, today),
+ ].filter(Boolean);
+
+ const decision = findings.reduce(
+ (current, finding) =>
+ DECISION_PRIORITY[finding.severity] > DECISION_PRIORITY[current]
+ ? finding.severity
+ : current,
+ "RELEASE",
+ );
+
+ return {
+ licenseId: packet.licenseId,
+ customer: packet.customer.name,
+ exportType: packet.export.type,
+ decision,
+ findings,
+ financeAction: financeActionFor(decision),
+ };
+}
+
+export function summarizeResults(results) {
+ return {
+ total: results.length,
+ release: results.filter((result) => result.decision === "RELEASE").length,
+ review: results.filter((result) => result.decision === "REVIEW").length,
+ hold: results.filter((result) => result.decision === "HOLD").length,
+ };
+}
+
+function checkEmbargo(packet, today) {
+ if (packet.export.embargoEndsAt <= today) {
+ return null;
+ }
+ if (packet.license.allowEarlyAccess && packet.license.earlyAccessApprovalId) {
+ return {
+ code: "EARLY_ACCESS_REVIEW",
+ severity: "REVIEW",
+ message: "Export is still under embargo but has an early-access approval.",
+ };
+ }
+ return {
+ code: "EMBARGO_ACTIVE",
+ severity: "HOLD",
+ message: `Export is embargoed until ${packet.export.embargoEndsAt}.`,
+ };
+}
+
+function checkExclusivity(packet) {
+ const exclusive = packet.license.exclusiveCustomerId;
+ if (!exclusive || exclusive === packet.customer.id) {
+ return null;
+ }
+ return {
+ code: "EXCLUSIVITY_CONFLICT",
+ severity: "HOLD",
+ message: "License conflicts with an active exclusive customer grant.",
+ };
+}
+
+function checkCohortSize(packet) {
+ if (packet.export.cohortSize >= packet.policy.minimumCohortSize) {
+ return null;
+ }
+ return {
+ code: "COHORT_TOO_SMALL",
+ severity: "HOLD",
+ message: `Cohort size ${packet.export.cohortSize} is below minimum ${packet.policy.minimumCohortSize}.`,
+ };
+}
+
+function checkConsentCoverage(packet) {
+ if (packet.export.consentCoverage >= packet.policy.minimumConsentCoverage) {
+ return null;
+ }
+ return {
+ code: "CONSENT_GAP",
+ severity: "REVIEW",
+ message: `Consent coverage ${packet.export.consentCoverage}% is below required ${packet.policy.minimumConsentCoverage}%.`,
+ };
+}
+
+function checkTier(packet) {
+ const requested = EXPORT_TIER_ORDER[packet.export.type] ?? Number.POSITIVE_INFINITY;
+ const allowed = EXPORT_TIER_ORDER[packet.license.maxExportType] ?? 0;
+ if (requested <= allowed) {
+ return null;
+ }
+ return {
+ code: "TIER_NOT_ENTITLED",
+ severity: "HOLD",
+ message: `${packet.license.tier} license does not permit ${packet.export.type} exports.`,
+ };
+}
+
+function checkTerms(packet) {
+ const missing = [];
+ if (!packet.license.hasDataProcessingAgreement) {
+ missing.push("data processing agreement");
+ }
+ if (!packet.export.redactionProfileId) {
+ missing.push("redaction profile");
+ }
+ if (!packet.export.attributionText) {
+ missing.push("attribution text");
+ }
+ if (missing.length === 0) {
+ return null;
+ }
+ return {
+ code: "MISSING_RELEASE_TERMS",
+ severity: "REVIEW",
+ message: `Missing ${missing.join(", ")}.`,
+ };
+}
+
+function checkSnapshotFreshness(packet, today) {
+ const snapshotAgeDays = daysBetween(packet.export.snapshotAt, today);
+ if (snapshotAgeDays <= packet.policy.maximumSnapshotAgeDays) {
+ return null;
+ }
+ return {
+ code: "STALE_SNAPSHOT",
+ severity: "REVIEW",
+ message: `Snapshot is ${snapshotAgeDays} days old; maximum is ${packet.policy.maximumSnapshotAgeDays}.`,
+ };
+}
+
+function daysBetween(from, to) {
+ const start = Date.parse(`${from}T00:00:00Z`);
+ const end = Date.parse(`${to}T00:00:00Z`);
+ return Math.floor((end - start) / 86_400_000);
+}
+
+function financeActionFor(decision) {
+ if (decision === "RELEASE") {
+ return "release_export_and_invoice";
+ }
+ if (decision === "REVIEW") {
+ return "route_to_licensing_operations";
+ }
+ return "hold_export_and_block_invoice";
+}
diff --git a/embargoed-data-license-guard/src/report.js b/embargoed-data-license-guard/src/report.js
new file mode 100644
index 00000000..bf4cd6b8
--- /dev/null
+++ b/embargoed-data-license-guard/src/report.js
@@ -0,0 +1,66 @@
+import { mkdirSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+import { summarizeResults } from "./licenseGuard.js";
+
+export function writeArtifacts(results, outputDir = "artifacts") {
+ mkdirSync(outputDir, { recursive: true });
+ writeFileSync(
+ join(outputDir, "demo-output.json"),
+ `${JSON.stringify(results, null, 2)}\n`,
+ );
+ writeFileSync(join(outputDir, "demo-report.md"), markdownReport(results));
+ writeFileSync(join(outputDir, "release-summary.svg"), svgSummary(results));
+}
+
+export function markdownReport(results) {
+ const rows = results
+ .map(
+ (result) =>
+ `| ${result.licenseId} | ${result.customer} | ${result.exportType} | ${result.decision} | ${result.financeAction} |`,
+ )
+ .join("\n");
+ const details = results
+ .map((result) => {
+ const findings = result.findings.length
+ ? result.findings
+ .map((finding) => ` - ${finding.severity}: ${finding.code} - ${finding.message}`)
+ .join("\n")
+ : " - RELEASE: no blocking findings";
+ return `### ${result.licenseId}\n\n${findings}`;
+ })
+ .join("\n\n");
+
+ return `# Embargoed Data License Guard Demo
+
+| License | Customer | Export | Decision | Finance action |
+| --- | --- | --- | --- | --- |
+${rows}
+
+${details}
+`;
+}
+
+export function svgSummary(results) {
+ const summary = summarizeResults(results);
+ const bars = [
+ { label: "Release", value: summary.release, color: "#15803d" },
+ { label: "Review", value: summary.review, color: "#ca8a04" },
+ { label: "Hold", value: summary.hold, color: "#b91c1c" },
+ ];
+ const max = Math.max(...bars.map((bar) => bar.value), 1);
+ const barMarkup = bars
+ .map((bar, index) => {
+ const y = 56 + index * 48;
+ const width = 240 * (bar.value / max);
+ return `${bar.label}${bar.value}`;
+ })
+ .join("");
+ return `
+`;
+}
diff --git a/embargoed-data-license-guard/src/samplePackets.js b/embargoed-data-license-guard/src/samplePackets.js
new file mode 100644
index 00000000..f9a82acc
--- /dev/null
+++ b/embargoed-data-license-guard/src/samplePackets.js
@@ -0,0 +1,80 @@
+export const samplePackets = [
+ {
+ licenseId: "lic-gov-neuro-2026",
+ customer: { id: "nih-policy-lab", name: "NIH Policy Lab" },
+ license: {
+ tier: "Government API",
+ maxExportType: "api",
+ allowEarlyAccess: false,
+ earlyAccessApprovalId: null,
+ exclusiveCustomerId: null,
+ hasDataProcessingAgreement: true,
+ },
+ export: {
+ type: "api",
+ embargoEndsAt: "2026-05-15",
+ snapshotAt: "2026-06-20",
+ cohortSize: 1840,
+ consentCoverage: 97,
+ redactionProfileId: "redact-standard-v3",
+ attributionText: "Derived from anonymized SCIBASE research metadata.",
+ },
+ policy: {
+ minimumCohortSize: 100,
+ minimumConsentCoverage: 95,
+ maximumSnapshotAgeDays: 30,
+ },
+ },
+ {
+ licenseId: "lic-market-rare-disease-2026",
+ customer: { id: "market-intel-a", name: "Market Intel A" },
+ license: {
+ tier: "Periodic Snapshot",
+ maxExportType: "periodic_snapshot",
+ allowEarlyAccess: true,
+ earlyAccessApprovalId: "ea-rare-disease-17",
+ exclusiveCustomerId: null,
+ hasDataProcessingAgreement: true,
+ },
+ export: {
+ type: "periodic_snapshot",
+ embargoEndsAt: "2026-07-15",
+ snapshotAt: "2026-06-12",
+ cohortSize: 84,
+ consentCoverage: 92,
+ redactionProfileId: "redact-rare-disease-v2",
+ attributionText: "Synthetic rare-disease trend extract for review.",
+ },
+ policy: {
+ minimumCohortSize: 75,
+ minimumConsentCoverage: 95,
+ maximumSnapshotAgeDays: 30,
+ },
+ },
+ {
+ licenseId: "lic-exclusive-funder-2026",
+ customer: { id: "regional-consortium", name: "Regional Consortium" },
+ license: {
+ tier: "Dashboard",
+ maxExportType: "dashboard",
+ allowEarlyAccess: false,
+ earlyAccessApprovalId: null,
+ exclusiveCustomerId: "national-funder",
+ hasDataProcessingAgreement: false,
+ },
+ export: {
+ type: "white_label",
+ embargoEndsAt: "2026-04-01",
+ snapshotAt: "2026-05-01",
+ cohortSize: 42,
+ consentCoverage: 88,
+ redactionProfileId: "",
+ attributionText: "",
+ },
+ policy: {
+ minimumCohortSize: 100,
+ minimumConsentCoverage: 95,
+ maximumSnapshotAgeDays: 30,
+ },
+ },
+];
diff --git a/embargoed-data-license-guard/test/license-guard.test.js b/embargoed-data-license-guard/test/license-guard.test.js
new file mode 100644
index 00000000..9c9816df
--- /dev/null
+++ b/embargoed-data-license-guard/test/license-guard.test.js
@@ -0,0 +1,50 @@
+import assert from "node:assert/strict";
+import { readFileSync, rmSync } from "node:fs";
+
+import {
+ evaluateLicenseRelease,
+ summarizeResults,
+} from "../src/licenseGuard.js";
+import { markdownReport, svgSummary, writeArtifacts } from "../src/report.js";
+import { samplePackets } from "../src/samplePackets.js";
+
+const [releasePacket, reviewPacket, holdPacket] = samplePackets;
+
+const releaseResult = evaluateLicenseRelease(releasePacket);
+assert.equal(releaseResult.decision, "RELEASE");
+
+const review = evaluateLicenseRelease(reviewPacket);
+assert.equal(review.decision, "REVIEW");
+assert.deepEqual(
+ review.findings.map((finding) => finding.code),
+ ["EARLY_ACCESS_REVIEW", "CONSENT_GAP"],
+);
+
+const hold = evaluateLicenseRelease(holdPacket);
+assert.equal(hold.decision, "HOLD");
+assert.ok(hold.findings.some((finding) => finding.code === "EXCLUSIVITY_CONFLICT"));
+assert.ok(hold.findings.some((finding) => finding.code === "TIER_NOT_ENTITLED"));
+
+assert.deepEqual(
+ summarizeResults(samplePackets.map((packet) => evaluateLicenseRelease(packet))),
+ { total: 3, release: 1, review: 1, hold: 1 },
+);
+
+const report = markdownReport([releaseResult, review, hold]);
+assert.match(report, /Embargoed Data License Guard Demo/);
+assert.match(report, /lic-market-rare-disease-2026/);
+
+const svg = svgSummary([releaseResult, review, hold]);
+assert.match(svg, /