diff --git a/analytics-api-license-usage-guard/README.md b/analytics-api-license-usage-guard/README.md
new file mode 100644
index 00000000..43ec2d96
--- /dev/null
+++ b/analytics-api-license-usage-guard/README.md
@@ -0,0 +1,33 @@
+# Analytics API License Usage Guard
+
+This module provides a focused Revenue Infrastructure slice for SCIBASE issue
+#20. It validates paid analytics API customers before graph metadata queries are
+served or billed.
+
+## What It Covers
+
+- License tier and active-license checks.
+- Dataset and topic entitlement checks.
+- Anonymization threshold enforcement for metadata-only exports.
+- Private-content blocking.
+- Monthly quota and overage authorization checks.
+- Per-minute burst throttling.
+- Deterministic revenue and audit packets for usage dashboards.
+
+## Run
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+Generated artifacts are written to `reports/`:
+
+- `summary.json`
+- `reviewer-packet.md`
+- `summary.svg`
+- `demo.mp4`
+
+All data is synthetic and does not call Stripe, PayPal, customer systems, live
+billing providers, or private analytics stores.
diff --git a/analytics-api-license-usage-guard/acceptance-notes.md b/analytics-api-license-usage-guard/acceptance-notes.md
new file mode 100644
index 00000000..e0202e7b
--- /dev/null
+++ b/analytics-api-license-usage-guard/acceptance-notes.md
@@ -0,0 +1,20 @@
+# Acceptance Notes
+
+The guard classifies analytics API requests as:
+
+- `allow`: query is licensed, anonymous, inside quota, and billable.
+- `bill_overage`: query can be served with authorized overage billing.
+- `throttle`: burst or quota limits prevent immediate service.
+- `block`: inactive license, disallowed package/topic, private content, or
+ anonymization failure.
+
+Validation commands:
+
+```bash
+npm run check
+npm test
+npm run demo
+ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height,pix_fmt -of default=noprint_wrappers=1 reports/demo.mp4
+git diff --check
+git diff --cached --check
+```
diff --git a/analytics-api-license-usage-guard/demo.js b/analytics-api-license-usage-guard/demo.js
new file mode 100644
index 00000000..657fd11d
--- /dev/null
+++ b/analytics-api-license-usage-guard/demo.js
@@ -0,0 +1,31 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const {
+ analyzeAnalyticsApiUsage,
+ renderMarkdownReport,
+ renderSvgSummary,
+} = require("./index");
+const { sampleAnalyticsApiPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const result = analyzeAnalyticsApiUsage(sampleAnalyticsApiPacket, {
+ asOf: "2026-05-22T12:00:00.000Z",
+});
+
+fs.writeFileSync(
+ path.join(reportsDir, "summary.json"),
+ `${JSON.stringify(result, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, "reviewer-packet.md"),
+ renderMarkdownReport(result)
+);
+fs.writeFileSync(
+ path.join(reportsDir, "summary.svg"),
+ renderSvgSummary(result)
+);
+
+console.log("analytics API license usage guard demo artifacts written");
+console.log(`audit digest: ${result.auditDigest}`);
diff --git a/analytics-api-license-usage-guard/index.js b/analytics-api-license-usage-guard/index.js
new file mode 100644
index 00000000..94825f28
--- /dev/null
+++ b/analytics-api-license-usage-guard/index.js
@@ -0,0 +1,310 @@
+const crypto = require("node:crypto");
+
+const ALLOW = "allow";
+const BILL_OVERAGE = "bill_overage";
+const THROTTLE = "throttle";
+const BLOCK = "block";
+
+function requireFields(value, fields, label) {
+ for (const field of fields) {
+ if (value[field] === undefined || value[field] === null || value[field] === "") {
+ throw new Error(`missing required ${label} field: ${field}`);
+ }
+ }
+}
+
+function stableStringify(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(",")}]`;
+ }
+
+ if (value && typeof value === "object") {
+ return `{${Object.keys(value)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(",")}}`;
+ }
+
+ return JSON.stringify(value);
+}
+
+function createAuditDigest(result) {
+ const payload = {
+ asOf: result.asOf,
+ totals: result.totals,
+ decisions: result.decisions.map((decision) => ({
+ id: decision.id,
+ customerId: decision.customerId,
+ status: decision.status,
+ billableUnits: decision.billableUnits,
+ reasons: decision.reasons,
+ })),
+ };
+
+ return crypto.createHash("sha256").update(stableStringify(payload)).digest("hex");
+}
+
+function licenseLookup(licenses) {
+ const lookup = new Map();
+ for (const license of licenses) {
+ requireFields(
+ license,
+ [
+ "customerId",
+ "tier",
+ "active",
+ "monthlyQuota",
+ "usedThisMonth",
+ "burstLimitPerMinute",
+ "allowedDatasets",
+ "allowedTopics",
+ "minimumCohortSize",
+ "overageAuthorized",
+ ],
+ "license"
+ );
+ lookup.set(license.customerId, license);
+ }
+ return lookup;
+}
+
+function evaluateRequest(request, licenses) {
+ requireFields(
+ request,
+ [
+ "id",
+ "customerId",
+ "dataset",
+ "topic",
+ "estimatedRows",
+ "cohortSize",
+ "queryUnits",
+ "requestsLastMinute",
+ "includesPrivateContent",
+ "requestedAt",
+ ],
+ "analytics request"
+ );
+
+ const license = licenses.get(request.customerId);
+ const reasons = [];
+ const actions = [];
+ const base = {
+ id: request.id,
+ customerId: request.customerId,
+ dataset: request.dataset,
+ topic: request.topic,
+ queryUnits: Number(request.queryUnits),
+ billableUnits: 0,
+ status: BLOCK,
+ reasons,
+ actions,
+ };
+
+ if (!license) {
+ return {
+ ...base,
+ reasons: ["customer has no analytics API license"],
+ actions: ["Block query and route account to revenue operations"],
+ };
+ }
+
+ if (!license.active) {
+ reasons.push("analytics API license is inactive");
+ }
+ if (!license.allowedDatasets.includes(request.dataset)) {
+ reasons.push("dataset is not included in the licensed analytics package");
+ }
+ if (!license.allowedTopics.includes(request.topic)) {
+ reasons.push("topic is outside licensed usage scope");
+ }
+ if (request.includesPrivateContent) {
+ reasons.push("query attempts to access private content rather than anonymized metadata");
+ }
+ if (request.cohortSize < license.minimumCohortSize) {
+ reasons.push("cohort size is below anonymization threshold");
+ }
+
+ if (reasons.length > 0) {
+ return {
+ ...base,
+ status: BLOCK,
+ reasons,
+ actions: [
+ "Block analytics response",
+ "Log revenue and privacy audit event",
+ "Show permitted datasets, topics, and anonymization requirements",
+ ],
+ };
+ }
+
+ if (request.requestsLastMinute > license.burstLimitPerMinute) {
+ return {
+ ...base,
+ status: THROTTLE,
+ reasons: ["request burst exceeds licensed per-minute API limit"],
+ actions: [
+ "Throttle query before execution",
+ "Preserve request for API usage dashboard",
+ ],
+ };
+ }
+
+ const projectedUsage = Number(license.usedThisMonth) + Number(request.queryUnits);
+ if (projectedUsage > Number(license.monthlyQuota)) {
+ if (!license.overageAuthorized) {
+ return {
+ ...base,
+ status: THROTTLE,
+ reasons: ["monthly query quota would be exceeded and overage is not authorized"],
+ actions: [
+ "Throttle query until renewal or account approval",
+ "Offer overage authorization workflow",
+ ],
+ };
+ }
+
+ return {
+ ...base,
+ status: BILL_OVERAGE,
+ billableUnits: base.queryUnits,
+ reasons: ["query exceeds quota but overage billing is authorized"],
+ actions: [
+ "Serve anonymized analytics response",
+ "Record overage units for invoice line item",
+ ],
+ };
+ }
+
+ return {
+ ...base,
+ status: ALLOW,
+ billableUnits: base.queryUnits,
+ reasons: ["query is inside license, privacy, quota, and burst limits"],
+ actions: [
+ "Serve anonymized analytics response",
+ "Record billable usage units",
+ ],
+ };
+}
+
+function analyzeAnalyticsApiUsage(packet, options = {}) {
+ requireFields(packet, ["licenses", "requests"], "analytics API packet");
+ if (!Array.isArray(packet.licenses) || !Array.isArray(packet.requests)) {
+ throw new Error("licenses and requests must be arrays");
+ }
+
+ const licenses = licenseLookup(packet.licenses);
+ const decisions = packet.requests.map((request) => evaluateRequest(request, licenses));
+ const totals = decisions.reduce(
+ (acc, decision) => {
+ acc.totalRequests += 1;
+ acc.billableUnits += decision.billableUnits;
+ acc.byStatus[decision.status] = (acc.byStatus[decision.status] || 0) + 1;
+ return acc;
+ },
+ {
+ totalRequests: 0,
+ billableUnits: 0,
+ byStatus: {},
+ }
+ );
+ const result = {
+ asOf: options.asOf || packet.asOf || new Date().toISOString(),
+ totals,
+ decisions,
+ };
+
+ return {
+ ...result,
+ auditDigest: createAuditDigest(result),
+ };
+}
+
+function renderMarkdownReport(result) {
+ const lines = [
+ "# Analytics API License Usage Guard",
+ "",
+ `As of: ${result.asOf}`,
+ `Audit digest: \`${result.auditDigest}\``,
+ "",
+ "## Totals",
+ "",
+ `- Requests evaluated: ${result.totals.totalRequests}`,
+ `- Billable units: ${result.totals.billableUnits}`,
+ ];
+
+ for (const [status, count] of Object.entries(result.totals.byStatus).sort()) {
+ lines.push(`- ${status}: ${count}`);
+ }
+
+ lines.push("", "## Query Decisions", "");
+ for (const decision of result.decisions) {
+ lines.push(
+ `### ${decision.id}`,
+ "",
+ `- Customer: ${decision.customerId}`,
+ `- Dataset: ${decision.dataset}`,
+ `- Topic: ${decision.topic}`,
+ `- Status: ${decision.status}`,
+ `- Billable units: ${decision.billableUnits}`,
+ `- Reasons: ${decision.reasons.join("; ")}`,
+ `- Actions: ${decision.actions.join("; ")}`,
+ ""
+ );
+ }
+
+ return `${lines.join("\n").trimEnd()}\n`;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function renderSvgSummary(result) {
+ const rows = Object.entries(result.totals.byStatus)
+ .sort()
+ .map(([status, count], index) => {
+ const y = 154 + index * 46;
+ const width = Math.max(44, count * 92);
+ return `${escapeXml(status)}
+
+ ${count}`;
+ })
+ .join("\n ");
+
+ return `
+`;
+}
+
+module.exports = {
+ ALLOW,
+ BILL_OVERAGE,
+ THROTTLE,
+ BLOCK,
+ analyzeAnalyticsApiUsage,
+ createAuditDigest,
+ renderMarkdownReport,
+ renderSvgSummary,
+};
diff --git a/analytics-api-license-usage-guard/package.json b/analytics-api-license-usage-guard/package.json
new file mode 100644
index 00000000..abb0f1e2
--- /dev/null
+++ b/analytics-api-license-usage-guard/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "analytics-api-license-usage-guard",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Synthetic analytics API license usage guard for SCIBASE issue #20.",
+ "main": "index.js",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js",
+ "test": "node test.js",
+ "demo": "node demo.js"
+ },
+ "license": "MIT"
+}
diff --git a/analytics-api-license-usage-guard/reports/demo.mp4 b/analytics-api-license-usage-guard/reports/demo.mp4
new file mode 100644
index 00000000..82071f02
Binary files /dev/null and b/analytics-api-license-usage-guard/reports/demo.mp4 differ
diff --git a/analytics-api-license-usage-guard/reports/reviewer-packet.md b/analytics-api-license-usage-guard/reports/reviewer-packet.md
new file mode 100644
index 00000000..1039245d
--- /dev/null
+++ b/analytics-api-license-usage-guard/reports/reviewer-packet.md
@@ -0,0 +1,84 @@
+# Analytics API License Usage Guard
+
+As of: 2026-05-22T12:00:00.000Z
+Audit digest: `56f75fde23be7ba664e9e122c1c43530a39beeb8dc3bb96db3981a20cff230f5`
+
+## Totals
+
+- Requests evaluated: 7
+- Billable units: 120
+- allow: 1
+- block: 4
+- throttle: 2
+
+## Query Decisions
+
+### query-001
+
+- Customer: cust-nih-policy
+- Dataset: citation_networks
+- Topic: public_health
+- Status: allow
+- Billable units: 120
+- Reasons: query is inside license, privacy, quota, and burst limits
+- Actions: Serve anonymized analytics response; Record billable usage units
+
+### query-002
+
+- Customer: cust-market-intel
+- Dataset: dataset_reuse
+- Topic: biotech
+- Status: block
+- Billable units: 0
+- Reasons: dataset is not included in the licensed analytics package
+- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements
+
+### query-003
+
+- Customer: cust-market-intel
+- Dataset: topic_trends
+- Topic: biotech
+- Status: block
+- Billable units: 0
+- Reasons: cohort size is below anonymization threshold
+- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements
+
+### query-004
+
+- Customer: cust-market-intel
+- Dataset: method_usage
+- Topic: materials
+- Status: throttle
+- Billable units: 0
+- Reasons: request burst exceeds licensed per-minute API limit
+- Actions: Throttle query before execution; Preserve request for API usage dashboard
+
+### query-005
+
+- Customer: cust-market-intel
+- Dataset: method_usage
+- Topic: materials
+- Status: throttle
+- Billable units: 0
+- Reasons: monthly query quota would be exceeded and overage is not authorized
+- Actions: Throttle query until renewal or account approval; Offer overage authorization workflow
+
+### query-006
+
+- Customer: cust-expired-consortium
+- Dataset: citation_networks
+- Topic: open_science
+- Status: block
+- Billable units: 0
+- Reasons: analytics API license is inactive
+- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements
+
+### query-007
+
+- Customer: cust-nih-policy
+- Dataset: dataset_reuse
+- Topic: grant_outcomes
+- Status: block
+- Billable units: 0
+- Reasons: query attempts to access private content rather than anonymized metadata
+- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements
diff --git a/analytics-api-license-usage-guard/reports/summary.json b/analytics-api-license-usage-guard/reports/summary.json
new file mode 100644
index 00000000..d1a710af
--- /dev/null
+++ b/analytics-api-license-usage-guard/reports/summary.json
@@ -0,0 +1,131 @@
+{
+ "asOf": "2026-05-22T12:00:00.000Z",
+ "totals": {
+ "totalRequests": 7,
+ "billableUnits": 120,
+ "byStatus": {
+ "allow": 1,
+ "block": 4,
+ "throttle": 2
+ }
+ },
+ "decisions": [
+ {
+ "id": "query-001",
+ "customerId": "cust-nih-policy",
+ "dataset": "citation_networks",
+ "topic": "public_health",
+ "queryUnits": 120,
+ "billableUnits": 120,
+ "status": "allow",
+ "reasons": [
+ "query is inside license, privacy, quota, and burst limits"
+ ],
+ "actions": [
+ "Serve anonymized analytics response",
+ "Record billable usage units"
+ ]
+ },
+ {
+ "id": "query-002",
+ "customerId": "cust-market-intel",
+ "dataset": "dataset_reuse",
+ "topic": "biotech",
+ "queryUnits": 70,
+ "billableUnits": 0,
+ "status": "block",
+ "reasons": [
+ "dataset is not included in the licensed analytics package"
+ ],
+ "actions": [
+ "Block analytics response",
+ "Log revenue and privacy audit event",
+ "Show permitted datasets, topics, and anonymization requirements"
+ ]
+ },
+ {
+ "id": "query-003",
+ "customerId": "cust-market-intel",
+ "dataset": "topic_trends",
+ "topic": "biotech",
+ "queryUnits": 60,
+ "billableUnits": 0,
+ "status": "block",
+ "reasons": [
+ "cohort size is below anonymization threshold"
+ ],
+ "actions": [
+ "Block analytics response",
+ "Log revenue and privacy audit event",
+ "Show permitted datasets, topics, and anonymization requirements"
+ ]
+ },
+ {
+ "id": "query-004",
+ "customerId": "cust-market-intel",
+ "dataset": "method_usage",
+ "topic": "materials",
+ "queryUnits": 40,
+ "billableUnits": 0,
+ "status": "throttle",
+ "reasons": [
+ "request burst exceeds licensed per-minute API limit"
+ ],
+ "actions": [
+ "Throttle query before execution",
+ "Preserve request for API usage dashboard"
+ ]
+ },
+ {
+ "id": "query-005",
+ "customerId": "cust-market-intel",
+ "dataset": "method_usage",
+ "topic": "materials",
+ "queryUnits": 75,
+ "billableUnits": 0,
+ "status": "throttle",
+ "reasons": [
+ "monthly query quota would be exceeded and overage is not authorized"
+ ],
+ "actions": [
+ "Throttle query until renewal or account approval",
+ "Offer overage authorization workflow"
+ ]
+ },
+ {
+ "id": "query-006",
+ "customerId": "cust-expired-consortium",
+ "dataset": "citation_networks",
+ "topic": "open_science",
+ "queryUnits": 35,
+ "billableUnits": 0,
+ "status": "block",
+ "reasons": [
+ "analytics API license is inactive"
+ ],
+ "actions": [
+ "Block analytics response",
+ "Log revenue and privacy audit event",
+ "Show permitted datasets, topics, and anonymization requirements"
+ ]
+ },
+ {
+ "id": "query-007",
+ "customerId": "cust-nih-policy",
+ "dataset": "dataset_reuse",
+ "topic": "grant_outcomes",
+ "queryUnits": 130,
+ "billableUnits": 0,
+ "status": "block",
+ "reasons": [
+ "query attempts to access private content rather than anonymized metadata"
+ ],
+ "actions": [
+ "Block analytics response",
+ "Log revenue and privacy audit event",
+ "Show permitted datasets, topics, and anonymization requirements"
+ ]
+ }
+ ],
+ "auditDigest": "56f75fde23be7ba664e9e122c1c43530a39beeb8dc3bb96db3981a20cff230f5"
+}
diff --git a/analytics-api-license-usage-guard/reports/summary.svg b/analytics-api-license-usage-guard/reports/summary.svg
new file mode 100644
index 00000000..e7a81804
--- /dev/null
+++ b/analytics-api-license-usage-guard/reports/summary.svg
@@ -0,0 +1,26 @@
+
diff --git a/analytics-api-license-usage-guard/requirements-map.md b/analytics-api-license-usage-guard/requirements-map.md
new file mode 100644
index 00000000..f211037d
--- /dev/null
+++ b/analytics-api-license-usage-guard/requirements-map.md
@@ -0,0 +1,18 @@
+# Requirements Map
+
+| Issue #20 area | Coverage |
+| --- | --- |
+| Licensing APIs & Analytics | Validates licensed metadata API query access before serving graph analytics. |
+| Usage-based pricing | Records billable query units and blocks unapproved overage. |
+| Real-time usage meters | Applies monthly quota and burst-limit enforcement. |
+| Data licensing models | Enforces dataset, topic, and anonymized-metadata restrictions. |
+| Secure payment integrations | Avoids live payment systems and emits audit packets that can feed finance review. |
+
+## Non-overlap
+
+This is not a generic billing, metering ledger, tax, dispute, SLA, royalty,
+forecast, FX, plan migration, procurement, invoice acceptance,
+sanctions/export-control, payment failover/webhook, trial/promotion, storage
+overage, account transfer, collections, or prepaid compute credit breakage
+slice. It focuses on paid analytics API license usage and query entitlement
+gating.
diff --git a/analytics-api-license-usage-guard/sample-data.js b/analytics-api-license-usage-guard/sample-data.js
new file mode 100644
index 00000000..7c73fd63
--- /dev/null
+++ b/analytics-api-license-usage-guard/sample-data.js
@@ -0,0 +1,129 @@
+const sampleAnalyticsApiPacket = {
+ asOf: "2026-05-22T12:00:00.000Z",
+ licenses: [
+ {
+ customerId: "cust-nih-policy",
+ tier: "government_policy",
+ active: true,
+ monthlyQuota: 10000,
+ usedThisMonth: 4200,
+ burstLimitPerMinute: 120,
+ allowedDatasets: ["citation_networks", "dataset_reuse", "reproducibility_scores"],
+ allowedTopics: ["public_health", "open_science", "grant_outcomes"],
+ minimumCohortSize: 25,
+ overageAuthorized: true,
+ },
+ {
+ customerId: "cust-market-intel",
+ tier: "market_intelligence",
+ active: true,
+ monthlyQuota: 2500,
+ usedThisMonth: 2480,
+ burstLimitPerMinute: 40,
+ allowedDatasets: ["topic_trends", "method_usage"],
+ allowedTopics: ["materials", "biotech"],
+ minimumCohortSize: 50,
+ overageAuthorized: false,
+ },
+ {
+ customerId: "cust-expired-consortium",
+ tier: "academic_consortium",
+ active: false,
+ monthlyQuota: 5000,
+ usedThisMonth: 1200,
+ burstLimitPerMinute: 80,
+ allowedDatasets: ["citation_networks"],
+ allowedTopics: ["open_science"],
+ minimumCohortSize: 30,
+ overageAuthorized: false,
+ },
+ ],
+ requests: [
+ {
+ id: "query-001",
+ customerId: "cust-nih-policy",
+ dataset: "citation_networks",
+ topic: "public_health",
+ estimatedRows: 800,
+ cohortSize: 80,
+ queryUnits: 120,
+ requestsLastMinute: 16,
+ includesPrivateContent: false,
+ requestedAt: "2026-05-22T10:00:00.000Z",
+ },
+ {
+ id: "query-002",
+ customerId: "cust-market-intel",
+ dataset: "dataset_reuse",
+ topic: "biotech",
+ estimatedRows: 200,
+ cohortSize: 80,
+ queryUnits: 70,
+ requestsLastMinute: 18,
+ includesPrivateContent: false,
+ requestedAt: "2026-05-22T10:04:00.000Z",
+ },
+ {
+ id: "query-003",
+ customerId: "cust-market-intel",
+ dataset: "topic_trends",
+ topic: "biotech",
+ estimatedRows: 140,
+ cohortSize: 18,
+ queryUnits: 60,
+ requestsLastMinute: 12,
+ includesPrivateContent: false,
+ requestedAt: "2026-05-22T10:08:00.000Z",
+ },
+ {
+ id: "query-004",
+ customerId: "cust-market-intel",
+ dataset: "method_usage",
+ topic: "materials",
+ estimatedRows: 100,
+ cohortSize: 65,
+ queryUnits: 40,
+ requestsLastMinute: 52,
+ includesPrivateContent: false,
+ requestedAt: "2026-05-22T10:10:00.000Z",
+ },
+ {
+ id: "query-005",
+ customerId: "cust-market-intel",
+ dataset: "method_usage",
+ topic: "materials",
+ estimatedRows: 250,
+ cohortSize: 65,
+ queryUnits: 75,
+ requestsLastMinute: 20,
+ includesPrivateContent: false,
+ requestedAt: "2026-05-22T10:12:00.000Z",
+ },
+ {
+ id: "query-006",
+ customerId: "cust-expired-consortium",
+ dataset: "citation_networks",
+ topic: "open_science",
+ estimatedRows: 150,
+ cohortSize: 90,
+ queryUnits: 35,
+ requestsLastMinute: 5,
+ includesPrivateContent: false,
+ requestedAt: "2026-05-22T10:14:00.000Z",
+ },
+ {
+ id: "query-007",
+ customerId: "cust-nih-policy",
+ dataset: "dataset_reuse",
+ topic: "grant_outcomes",
+ estimatedRows: 120,
+ cohortSize: 64,
+ queryUnits: 130,
+ requestsLastMinute: 10,
+ includesPrivateContent: true,
+ requestedAt: "2026-05-22T10:16:00.000Z",
+ },
+ ],
+};
+
+module.exports = { sampleAnalyticsApiPacket };
diff --git a/analytics-api-license-usage-guard/test.js b/analytics-api-license-usage-guard/test.js
new file mode 100644
index 00000000..78126d39
--- /dev/null
+++ b/analytics-api-license-usage-guard/test.js
@@ -0,0 +1,52 @@
+const assert = require("node:assert/strict");
+const {
+ ALLOW,
+ BLOCK,
+ THROTTLE,
+ analyzeAnalyticsApiUsage,
+ createAuditDigest,
+ renderMarkdownReport,
+ renderSvgSummary,
+} = require("./index");
+const { sampleAnalyticsApiPacket } = require("./sample-data");
+
+const result = analyzeAnalyticsApiUsage(sampleAnalyticsApiPacket);
+
+assert.equal(result.totals.totalRequests, 7);
+assert.equal(result.totals.billableUnits, 120);
+assert.equal(result.totals.byStatus[ALLOW], 1);
+assert.equal(result.totals.byStatus[BLOCK], 4);
+assert.equal(result.totals.byStatus[THROTTLE], 2);
+
+const allowed = result.decisions.find((decision) => decision.id === "query-001");
+assert.equal(allowed.status, ALLOW);
+assert.equal(allowed.billableUnits, 120);
+
+const datasetBlock = result.decisions.find((decision) => decision.id === "query-002");
+assert.equal(datasetBlock.status, BLOCK);
+assert.match(datasetBlock.reasons.join(" "), /not included/);
+
+const cohortBlock = result.decisions.find((decision) => decision.id === "query-003");
+assert.equal(cohortBlock.status, BLOCK);
+assert.match(cohortBlock.reasons.join(" "), /anonymization threshold/);
+
+const burstThrottle = result.decisions.find((decision) => decision.id === "query-004");
+assert.equal(burstThrottle.status, THROTTLE);
+
+const quotaThrottle = result.decisions.find((decision) => decision.id === "query-005");
+assert.equal(quotaThrottle.status, THROTTLE);
+
+const privateBlock = result.decisions.find((decision) => decision.id === "query-007");
+assert.equal(privateBlock.status, BLOCK);
+assert.match(privateBlock.reasons.join(" "), /private content/);
+
+assert.equal(createAuditDigest(result), result.auditDigest);
+assert.match(renderMarkdownReport(result), /Analytics API License Usage Guard/);
+assert.match(renderSvgSummary(result), /Billable units 120/);
+
+assert.throws(
+ () => analyzeAnalyticsApiUsage({ licenses: [], requests: [{}] }),
+ /missing required analytics request field: id/
+);
+
+console.log("analytics API license usage guard tests passed");