diff --git a/README.md b/README.md index d338cf68..0f91fed8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # deepevents.ai deepevents.ai main codebase + +## Revenue Infrastructure Slices + +- [Partner Royalty Settlement Guard](partner-royalty-settlement-guard/README.md) - audit-ready settlement checks for data-licensing and white-label analytics revenue shares. diff --git a/partner-royalty-settlement-guard/README.md b/partner-royalty-settlement-guard/README.md new file mode 100644 index 00000000..8a45b310 --- /dev/null +++ b/partner-royalty-settlement-guard/README.md @@ -0,0 +1,45 @@ +# Partner Royalty Settlement Guard + +Self-contained Revenue Infrastructure slice for issue #20. It focuses on a narrow gap that is different from the existing subscription, metering, tax, dispute, SLA, procurement, pricing experiment, renewal, margin, and privacy-gate modules: audit-ready settlement of data-licensing and white-label analytics revenue shares. + +The guard evaluates each license invoice before money is released to dataset contributors, reseller partners, and the platform. + +## What It Does + +- Calculates platform fees, reseller fees, contributor royalty pools, reserves, and payable lines. +- Blocks duplicate invoices, expired agreements, unauthorized use cases, missing consent attestations, excessive reseller fees, and broken royalty split percentages. +- Holds otherwise-valid settlements when anonymized subject counts are below the configured aggregation floor or payout lines are below the minimum payout threshold. +- Produces a deterministic SHA-256 audit digest that finance can archive with the invoice packet. +- Returns reviewer-ready actions for settlement, hold, and blocker states. + +## Files + +- `index.js` - dependency-free settlement evaluator. +- `test.js` - Node assertions covering ready, held, blocked, duplicate, and validation paths. +- `demo.js` - terminal demo over a synthetic license batch. +- `requirements-map.md` - explicit mapping to issue #20 requirements. +- `acceptance-notes.md` - verification notes and limitations. +- `demo.svg` / `demo.mp4` - short visual demo artifacts for bounty review. + +## Run + +```bash +node partner-royalty-settlement-guard/test.js +node partner-royalty-settlement-guard/demo.js +``` + +## Example Output + +```text +Partner Royalty Settlement Guard Demo +===================================== +settlements: 3 +ready: 1, held: 1, blocked: 1 +ready payouts: $925.00 +held payouts: $232.56 +blocked gross: $640.00 +``` + +## Design Notes + +The module is synthetic-data-only and has no network, payment, Stripe, PayPal, bank, or credential integration. It is intended to be the policy and audit layer that would run before a real payout provider is called. diff --git a/partner-royalty-settlement-guard/acceptance-notes.md b/partner-royalty-settlement-guard/acceptance-notes.md new file mode 100644 index 00000000..7532d5da --- /dev/null +++ b/partner-royalty-settlement-guard/acceptance-notes.md @@ -0,0 +1,23 @@ +# Acceptance Notes + +## Validation + +Run from the repository root: + +```bash +node partner-royalty-settlement-guard/test.js +node partner-royalty-settlement-guard/demo.js +ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 partner-royalty-settlement-guard/demo.mp4 +``` + +## Expected Review Signals + +- Ready settlement releases payout lines and archives the digest. +- Low anonymized subject count becomes a hold, not a blocker, so finance can aggregate more usage. +- Duplicate invoice, expired agreement, missing consent, unauthorized use case, and split mismatch block release. +- Audit digest remains stable for identical input. +- All calculations use integer cents. + +## Scope Boundary + +This is an audit and policy module. It intentionally stops before live payout-provider integration so the demo remains credential-free and safe to run in CI or during review. diff --git a/partner-royalty-settlement-guard/demo.js b/partner-royalty-settlement-guard/demo.js new file mode 100644 index 00000000..916a00b9 --- /dev/null +++ b/partner-royalty-settlement-guard/demo.js @@ -0,0 +1,100 @@ +"use strict"; + +const { evaluateSettlement, money } = require("./index"); + +const sampleBatch = { + agreements: [ + { + id: "agr-national-knowledge-graph", + datasetId: "dataset-citation-network-2026q2", + status: "active", + endsAt: "2027-03-31", + platformFeeRate: 0.18, + resellerFeeRate: 0.08, + allowedUseCases: ["policy-planning", "meta-research"], + contributorRoyaltySplits: [ + { contributorId: "lab-north", share: 0.45 }, + { contributorId: "lab-south", share: 0.35 }, + { contributorId: "data-trust", share: 0.2 }, + ], + }, + { + id: "agr-white-label-pilot", + datasetId: "dataset-method-trends", + status: "active", + endsAt: "2026-09-30", + platformFeeRate: 0.2, + resellerFeeRate: 0.12, + allowedUseCases: ["white-label-dashboard"], + contributorRoyaltySplits: [ + { contributorId: "methods-lab", share: 0.5 }, + { contributorId: "instrument-core", share: 0.5 }, + ], + }, + { + id: "agr-expired-consortium", + datasetId: "dataset-private-notes", + status: "active", + endsAt: "2026-01-31", + allowedUseCases: ["market-intelligence"], + contributorRoyaltySplits: [{ contributorId: "legacy-lab", share: 1 }], + }, + ], + licenseEvents: [ + { + id: "lic-001", + invoiceId: "INV-2026-05-101", + agreementId: "agr-national-knowledge-graph", + customerId: "nih-policy-office", + datasetId: "dataset-citation-network-2026q2", + useCase: "policy-planning", + grossCents: 125000, + subjectCount: 480, + consentAttestation: true, + }, + { + id: "lic-002", + invoiceId: "INV-2026-05-102", + agreementId: "agr-white-label-pilot", + customerId: "research-dashboard-reseller", + datasetId: "dataset-method-trends", + useCase: "white-label-dashboard", + grossCents: 36000, + subjectCount: 18, + consentAttestation: true, + }, + { + id: "lic-003", + invoiceId: "INV-2026-05-101", + agreementId: "agr-expired-consortium", + customerId: "market-intel-firm", + datasetId: "dataset-private-notes", + useCase: "market-intelligence", + grossCents: 64000, + subjectCount: 72, + consentAttestation: false, + }, + ], +}; + +const report = evaluateSettlement(sampleBatch); + +console.log("Partner Royalty Settlement Guard Demo"); +console.log("====================================="); +console.log(`settlements: ${report.settlementCount}`); +console.log(`ready: ${report.totals.readyCount}, held: ${report.totals.heldCount}, blocked: ${report.totals.blockedCount}`); +console.log(`ready payouts: ${money(report.totals.readyPayoutCents)}`); +console.log(`held payouts: ${money(report.totals.heldPayoutCents)}`); +console.log(`blocked gross: ${money(report.totals.blockedGrossCents)}`); +console.log(`audit digest: ${report.auditDigest.slice(0, 16)}...`); + +for (const settlement of report.settlements) { + console.log(""); + console.log(`${settlement.eventId} ${settlement.status.toUpperCase()} ${money(settlement.grossCents)} ${settlement.invoiceId}`); + for (const finding of settlement.findings) { + console.log(`- ${finding.severity}: ${finding.code} - ${finding.message}`); + } + for (const action of settlement.recommendedActions) { + console.log(` action: ${action}`); + } +} diff --git a/partner-royalty-settlement-guard/demo.mp4 b/partner-royalty-settlement-guard/demo.mp4 new file mode 100644 index 00000000..9c484b6d Binary files /dev/null and b/partner-royalty-settlement-guard/demo.mp4 differ diff --git a/partner-royalty-settlement-guard/demo.svg b/partner-royalty-settlement-guard/demo.svg new file mode 100644 index 00000000..e7a04e1b --- /dev/null +++ b/partner-royalty-settlement-guard/demo.svg @@ -0,0 +1,32 @@ + + + + Partner Royalty Settlement Guard + Revenue Infrastructure slice for data-licensing and white-label analytics payouts + + + + READY + $925.00 + release payout lines + + + + + HELD + $232.56 + aggregation floor + + + + + BLOCKED + $640.00 + duplicate + consent + + + + Audit controls + duplicate invoice protection - split checks - consent gates - reserve recommendations - stable SHA-256 digest + Validation: node test.js | node demo.js | ffprobe demo.mp4 + diff --git a/partner-royalty-settlement-guard/index.js b/partner-royalty-settlement-guard/index.js new file mode 100644 index 00000000..fd84b0a4 --- /dev/null +++ b/partner-royalty-settlement-guard/index.js @@ -0,0 +1,288 @@ +"use strict"; + +const crypto = require("crypto"); + +const DEFAULT_POLICY = Object.freeze({ + asOf: "2026-05-20", + platformFeeRate: 0.18, + reserveRate: 0.05, + minimumPayoutCents: 5000, + minimumAggregationSubjects: 25, + maximumResellerFeeRate: 0.3, +}); + +function evaluateSettlement(input, policyOverrides = {}) { + const policy = { ...DEFAULT_POLICY, ...policyOverrides }; + const agreements = new Map((input.agreements || []).map((agreement) => [agreement.id, agreement])); + const seenInvoices = new Set(); + const settlements = []; + + for (const event of input.licenseEvents || []) { + const agreement = agreements.get(event.agreementId); + const findings = []; + + if (!agreement) { + findings.push(blocker("missing_agreement", `No agreement found for ${event.agreementId}`)); + settlements.push(buildBlockedSettlement(event, findings)); + continue; + } + + const grossCents = assertCents(event.grossCents, event.id, "grossCents"); + const platformFeeRate = rateOrDefault(agreement.platformFeeRate, policy.platformFeeRate); + const resellerFeeRate = rateOrDefault(event.resellerFeeRate, agreement.resellerFeeRate || 0); + const contributorSplits = agreement.contributorRoyaltySplits || []; + + if (seenInvoices.has(event.invoiceId)) { + findings.push(blocker("duplicate_invoice", `Invoice ${event.invoiceId} already appeared in this settlement batch`)); + } + seenInvoices.add(event.invoiceId); + + if (agreement.status !== "active") { + findings.push(blocker("inactive_agreement", `Agreement ${agreement.id} has status ${agreement.status || "unknown"}`)); + } + + if (agreement.endsAt && agreement.endsAt < policy.asOf) { + findings.push(blocker("expired_agreement", `Agreement expired on ${agreement.endsAt}`)); + } + + if (event.datasetId !== agreement.datasetId) { + findings.push(blocker("dataset_mismatch", `Event dataset ${event.datasetId} does not match agreement dataset ${agreement.datasetId}`)); + } + + if (!event.consentAttestation) { + findings.push(blocker("missing_consent", "Consent attestation is required before licensing revenue can be settled")); + } + + if ((event.subjectCount || 0) < policy.minimumAggregationSubjects) { + findings.push( + hold( + "aggregation_floor", + `Only ${event.subjectCount || 0} subjects are represented; minimum is ${policy.minimumAggregationSubjects}` + ) + ); + } + + if (agreement.allowedUseCases && !agreement.allowedUseCases.includes(event.useCase)) { + findings.push(blocker("use_case_not_allowed", `${event.useCase} is not allowed by agreement ${agreement.id}`)); + } + + if (resellerFeeRate > policy.maximumResellerFeeRate) { + findings.push( + blocker( + "reseller_fee_cap", + `Reseller fee ${percent(resellerFeeRate)} exceeds cap ${percent(policy.maximumResellerFeeRate)}` + ) + ); + } + + const splitTotal = roundRate(contributorSplits.reduce((sum, split) => sum + Number(split.share || 0), 0)); + if (contributorSplits.length === 0) { + findings.push(blocker("missing_contributor_splits", "At least one contributor split is required")); + } else if (splitTotal !== 1) { + findings.push(blocker("split_mismatch", `Contributor splits total ${percent(splitTotal)}, expected 100%`)); + } + + const platformFeeCents = roundCents(grossCents * platformFeeRate); + const resellerFeeCents = roundCents(grossCents * resellerFeeRate); + const royaltyPoolCents = Math.max(0, grossCents - platformFeeCents - resellerFeeCents); + const reserveCents = findings.some((finding) => finding.severity !== "info") + ? roundCents(royaltyPoolCents * policy.reserveRate) + : 0; + const distributableCents = royaltyPoolCents - reserveCents; + + const payoutLines = contributorSplits.map((split) => { + const payableCents = roundCents(distributableCents * Number(split.share || 0)); + const needsHold = payableCents > 0 && payableCents < policy.minimumPayoutCents; + if (needsHold) { + findings.push( + hold( + "minimum_payout", + `${split.contributorId} has ${money(payableCents)}, below minimum payout ${money(policy.minimumPayoutCents)}` + ) + ); + } + return { + contributorId: split.contributorId, + share: roundRate(Number(split.share || 0)), + payableCents, + status: needsHold ? "held" : "payable", + }; + }); + + const status = settlementStatus(findings); + settlements.push({ + eventId: event.id, + invoiceId: event.invoiceId, + agreementId: agreement.id, + customerId: event.customerId, + datasetId: event.datasetId, + useCase: event.useCase, + status, + grossCents, + platformFeeCents, + resellerFeeCents, + royaltyPoolCents, + reserveCents, + distributableCents: status === "blocked" ? 0 : distributableCents, + payoutLines: status === "blocked" ? payoutLines.map((line) => ({ ...line, status: "blocked" })) : payoutLines, + findings, + recommendedActions: recommendedActions(status, findings), + }); + } + + const totals = summarize(settlements); + const report = { + asOf: policy.asOf, + policy, + settlementCount: settlements.length, + totals, + settlements, + }; + + return { + ...report, + auditDigest: digest(report), + }; +} + +function buildBlockedSettlement(event, findings) { + return { + eventId: event.id, + invoiceId: event.invoiceId, + agreementId: event.agreementId, + customerId: event.customerId, + datasetId: event.datasetId, + useCase: event.useCase, + status: "blocked", + grossCents: assertCents(event.grossCents || 0, event.id, "grossCents"), + platformFeeCents: 0, + resellerFeeCents: 0, + royaltyPoolCents: 0, + reserveCents: 0, + distributableCents: 0, + payoutLines: [], + findings, + recommendedActions: recommendedActions("blocked", findings), + }; +} + +function settlementStatus(findings) { + if (findings.some((finding) => finding.severity === "blocker")) return "blocked"; + if (findings.some((finding) => finding.severity === "hold")) return "held"; + return "ready"; +} + +function summarize(settlements) { + const totals = { + grossCents: 0, + platformFeeCents: 0, + resellerFeeCents: 0, + reserveCents: 0, + readyPayoutCents: 0, + heldPayoutCents: 0, + blockedGrossCents: 0, + readyCount: 0, + heldCount: 0, + blockedCount: 0, + }; + + for (const settlement of settlements) { + totals.grossCents += settlement.grossCents; + totals.platformFeeCents += settlement.platformFeeCents; + totals.resellerFeeCents += settlement.resellerFeeCents; + totals.reserveCents += settlement.reserveCents; + + if (settlement.status === "ready") { + totals.readyCount += 1; + totals.readyPayoutCents += settlement.payoutLines.reduce((sum, line) => sum + line.payableCents, 0); + } else if (settlement.status === "held") { + totals.heldCount += 1; + totals.heldPayoutCents += settlement.payoutLines.reduce((sum, line) => sum + line.payableCents, 0); + } else { + totals.blockedCount += 1; + totals.blockedGrossCents += settlement.grossCents; + } + } + + return totals; +} + +function recommendedActions(status, findings) { + if (status === "ready") { + return ["Release payout lines", "Archive the audit digest with the invoice packet"]; + } + + const actions = new Set(); + for (const finding of findings) { + if (finding.code === "missing_consent") actions.add("Collect consent attestation before settlement"); + if (finding.code === "aggregation_floor") actions.add("Hold settlement until additional anonymized usage is aggregated"); + if (finding.code === "duplicate_invoice") actions.add("Reconcile duplicate invoice before payout"); + if (finding.code === "expired_agreement") actions.add("Renew or replace the agreement before recognizing revenue"); + if (finding.code === "split_mismatch") actions.add("Correct royalty split percentages to total 100%"); + if (finding.code === "minimum_payout") actions.add("Accrue small royalty lines until the payout threshold is reached"); + if (finding.code === "use_case_not_allowed") actions.add("Obtain written approval for the requested license use case"); + if (finding.code === "reseller_fee_cap") actions.add("Cap or approve reseller fee before settlement"); + } + return Array.from(actions); +} + +function blocker(code, message) { + return { severity: "blocker", code, message }; +} + +function hold(code, message) { + return { severity: "hold", code, message }; +} + +function assertCents(value, eventId, field) { + if (!Number.isInteger(value) || value < 0) { + throw new Error(`${eventId || "settlement"} ${field} must be a non-negative integer number of cents`); + } + return value; +} + +function rateOrDefault(value, fallback) { + if (value === undefined || value === null) return fallback; + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric < 0) { + throw new Error(`Invalid rate: ${value}`); + } + return numeric; +} + +function roundCents(value) { + return Math.round(value); +} + +function roundRate(value) { + return Math.round(value * 10000) / 10000; +} + +function percent(value) { + return `${(value * 100).toFixed(2)}%`; +} + +function money(cents) { + return `$${(cents / 100).toFixed(2)}`; +} + +function digest(value) { + return crypto.createHash("sha256").update(canonicalJson(value)).digest("hex"); +} + +function canonicalJson(value) { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +module.exports = { + DEFAULT_POLICY, + evaluateSettlement, + money, +}; diff --git a/partner-royalty-settlement-guard/requirements-map.md b/partner-royalty-settlement-guard/requirements-map.md new file mode 100644 index 00000000..972f6e1a --- /dev/null +++ b/partner-royalty-settlement-guard/requirements-map.md @@ -0,0 +1,19 @@ +# Requirement Map + +Issue #20 asks for modular revenue infrastructure spanning subscription billing, AI compute billing, and licensing APIs and analytics. This slice targets the licensing and analytics revenue stream after a license invoice exists but before payout settlement. + +| Issue requirement | Coverage in this module | +| --- | --- | +| Data licensing models for institutional customers and government partners | Models partner/customer license events against agreement metadata and allowed use cases. | +| Licensing APIs and analytics | Treats white-label analytics and policy-planning exports as license events with dataset IDs, customers, use cases, and invoice IDs. | +| Monetize structured, anonymized usage and research metadata | Enforces a minimum aggregation subject floor and consent attestation before revenue can be settled. | +| Predictable recurring revenue and high margins | Calculates platform fees, reseller fees, contributor pools, reserves, and held payouts in cents. | +| Secure payment integrations and institutional invoicing | Produces pre-payment settlement decisions and audit-ready recommended actions without touching live payment rails. | +| Volume/consortium pricing and partner channels | Supports reseller fee rates, contributor royalty splits, payout thresholds, and duplicate invoice protection. | +| Compliance and audit readiness | Emits deterministic audit digests, blocker/hold findings, and finance actions for each settlement. | + +## Non-Goals + +- Does not integrate with Stripe, PayPal, banks, or Algora. +- Does not store real customer, contributor, or payment data. +- Does not duplicate existing broad subscription billing, dispute, tax, SLA, renewal, pricing experiment, procurement, margin, or privacy-safe licensing gate modules. diff --git a/partner-royalty-settlement-guard/test.js b/partner-royalty-settlement-guard/test.js new file mode 100644 index 00000000..252da063 --- /dev/null +++ b/partner-royalty-settlement-guard/test.js @@ -0,0 +1,110 @@ +"use strict"; + +const assert = require("assert/strict"); +const { evaluateSettlement } = require("./index"); + +function baseAgreement(overrides = {}) { + return { + id: "agreement-a", + datasetId: "dataset-a", + status: "active", + endsAt: "2026-12-31", + platformFeeRate: 0.18, + resellerFeeRate: 0.08, + allowedUseCases: ["policy-planning"], + contributorRoyaltySplits: [ + { contributorId: "lab-a", share: 0.6 }, + { contributorId: "lab-b", share: 0.4 }, + ], + ...overrides, + }; +} + +function baseEvent(overrides = {}) { + return { + id: "event-a", + invoiceId: "INV-A", + agreementId: "agreement-a", + customerId: "customer-a", + datasetId: "dataset-a", + useCase: "policy-planning", + grossCents: 100000, + subjectCount: 80, + consentAttestation: true, + ...overrides, + }; +} + +{ + const report = evaluateSettlement({ + agreements: [baseAgreement()], + licenseEvents: [baseEvent()], + }); + + assert.equal(report.settlements[0].status, "ready"); + assert.equal(report.totals.readyCount, 1); + assert.equal(report.totals.readyPayoutCents, 74000); + assert.equal(report.settlements[0].payoutLines[0].payableCents, 44400); + assert.match(report.auditDigest, /^[a-f0-9]{64}$/); +} + +{ + const first = evaluateSettlement({ + agreements: [baseAgreement()], + licenseEvents: [baseEvent()], + }); + const second = evaluateSettlement({ + agreements: [baseAgreement()], + licenseEvents: [baseEvent()], + }); + assert.equal(first.auditDigest, second.auditDigest); +} + +{ + const report = evaluateSettlement({ + agreements: [baseAgreement()], + licenseEvents: [baseEvent({ subjectCount: 8 })], + }); + + assert.equal(report.settlements[0].status, "held"); + assert.equal(report.totals.heldCount, 1); + assert.equal(report.settlements[0].reserveCents, 3700); + assert.ok(report.settlements[0].findings.some((finding) => finding.code === "aggregation_floor")); +} + +{ + const report = evaluateSettlement({ + agreements: [baseAgreement({ contributorRoyaltySplits: [{ contributorId: "lab-a", share: 0.7 }] })], + licenseEvents: [baseEvent()], + }); + + assert.equal(report.settlements[0].status, "blocked"); + assert.ok(report.settlements[0].findings.some((finding) => finding.code === "split_mismatch")); +} + +{ + const report = evaluateSettlement({ + agreements: [baseAgreement()], + licenseEvents: [ + baseEvent({ id: "event-a", invoiceId: "INV-DUP" }), + baseEvent({ id: "event-b", invoiceId: "INV-DUP", grossCents: 22000 }), + ], + }); + + assert.equal(report.settlements[0].status, "ready"); + assert.equal(report.settlements[1].status, "blocked"); + assert.ok(report.settlements[1].findings.some((finding) => finding.code === "duplicate_invoice")); +} + +{ + assert.throws( + () => + evaluateSettlement({ + agreements: [baseAgreement()], + licenseEvents: [baseEvent({ grossCents: 10.5 })], + }), + /grossCents must be a non-negative integer/ + ); +} + +console.log("partner-royalty-settlement-guard tests passed");