diff --git a/sponsor-escrow-readiness-guard/README.md b/sponsor-escrow-readiness-guard/README.md
new file mode 100644
index 00000000..ac595f27
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/README.md
@@ -0,0 +1,28 @@
+# Sponsor Escrow Readiness Guard
+
+This module adds a focused Scientific Bounty System guard for sponsor-side prize funding before a challenge is opened for solver submissions.
+
+It evaluates synthetic challenge funding packets for:
+
+- missing escrow or funding evidence
+- underfunded prize pools
+- payout schedules that do not reconcile to the prize amount
+- funding evidence that expires before the challenge award window
+- missing sponsor authorization for prize release
+- milestone schedules that omit solver-safe partial payment routing
+
+The output is deterministic and reviewer-ready: `release`, `revise`, or `hold`, plus remediation text for each finding.
+
+## Run
+
+```bash
+npm test
+npm run demo
+npm run demo:video
+```
+
+Generated artifacts are written to `reports/`.
+
+## Safety
+
+The sample packets are synthetic. The module does not call external services and does not use credentials, wallets, bank accounts, live escrow providers, payment processors, private challenge data, or production payout systems.
diff --git a/sponsor-escrow-readiness-guard/demo.js b/sponsor-escrow-readiness-guard/demo.js
new file mode 100644
index 00000000..79eb4d29
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/demo.js
@@ -0,0 +1,62 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+const { evaluateSponsorEscrowPacket } = require("./index");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const clean = evaluateSponsorEscrowPacket(cleanPacket);
+const risky = evaluateSponsorEscrowPacket(riskyPacket);
+const report = {
+ generatedAt: new Date("2026-06-02T00:00:00Z").toISOString(),
+ module: "sponsor-escrow-readiness-guard",
+ clean,
+ risky
+};
+
+function findingsMarkdown(result) {
+ return result.challengeResults.map((challenge) => {
+ const findings = challenge.findings.length === 0
+ ? "- No findings."
+ : challenge.findings.map((item) => `- ${item.severity.toUpperCase()} ${item.code}: ${item.message} ${item.remediation}`).join("\n");
+ return `### ${challenge.challengeId}\n\nDecision: ${challenge.decision}\n\n${findings}`;
+ }).join("\n\n");
+}
+
+const markdown = `# Sponsor Escrow Readiness Report
+
+Generated: ${report.generatedAt}
+
+## Clean Packet
+
+Decision: ${clean.decision}
+
+${findingsMarkdown(clean)}
+
+## Risky Packet
+
+Decision: ${risky.decision}
+
+${findingsMarkdown(risky)}
+`;
+
+const svg = ``;
+
+fs.writeFileSync(path.join(reportsDir, "escrow-readiness-report.json"), JSON.stringify(report, null, 2));
+fs.writeFileSync(path.join(reportsDir, "escrow-readiness-report.md"), markdown);
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg);
+
+console.log(`wrote ${path.join(reportsDir, "escrow-readiness-report.json")}`);
+console.log(`wrote ${path.join(reportsDir, "escrow-readiness-report.md")}`);
+console.log(`wrote ${path.join(reportsDir, "summary.svg")}`);
diff --git a/sponsor-escrow-readiness-guard/demo_video.py b/sponsor-escrow-readiness-guard/demo_video.py
new file mode 100644
index 00000000..77f0b919
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/demo_video.py
@@ -0,0 +1,88 @@
+import os
+from pathlib import Path
+import subprocess
+
+ROOT = Path(__file__).resolve().parent
+REPORTS = ROOT / "reports"
+FRAMES = REPORTS / "frames"
+
+REPORTS.mkdir(exist_ok=True)
+FRAMES.mkdir(exist_ok=True)
+
+SCENES = [
+ ("CHECK", "0x2563eb", "Sponsor prize evidence is reconciled before launch"),
+ ("RELEASE", "0x16a34a", "Clean challenge has funded escrow and payout routing"),
+ ("HOLD", "0xdc2626", "Underfunded or unauthorized prizes are blocked")
+]
+
+gif_path = REPORTS / "demo.gif"
+mp4_path = REPORTS / "demo.mp4"
+
+ffmpeg = os.environ.get("FFMPEG")
+if not ffmpeg:
+ candidates = list((ROOT.parents[2] / "tools" / "video-gen").glob("node_modules/**/ffmpeg.exe"))
+ ffmpeg = str(candidates[0]) if candidates else "ffmpeg"
+
+font = "C\\:/Windows/Fonts/arial.ttf"
+
+for index, (label, color, subtitle) in enumerate(SCENES):
+ frame_path = FRAMES / f"frame-{index:03d}.png"
+ filters = (
+ "drawbox=x=60:y=56:w=800:h=408:color=0xf8fafc:t=fill,"
+ "drawtext=fontfile='{font}':text='Sponsor Escrow Readiness Guard':x=104:y=118:fontsize=34:fontcolor=0x0f172a,"
+ "drawtext=fontfile='{font}':text='Decision lane\\: {label}':x=104:y=190:fontsize=30:fontcolor={color},"
+ "drawtext=fontfile='{font}':text='{subtitle}':x=104:y=248:fontsize=22:fontcolor=0x334155,"
+ "drawbox=x=104:y=318:w={bar}:h=54:color={color}:t=fill,"
+ "drawtext=fontfile='{font}':text='Synthetic data only - no live payments or credentials':x=104:y=414:fontsize=18:fontcolor=0x475569"
+ ).format(font=font, label=label, subtitle=subtitle, color=color, bar=260 + index * 120)
+ subprocess.run(
+ [
+ ffmpeg,
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0x0f172a:s=920x520:d=1",
+ "-vf",
+ filters,
+ "-frames:v",
+ "1",
+ str(frame_path),
+ ],
+ check=True,
+ )
+
+subprocess.run(
+ [
+ ffmpeg,
+ "-y",
+ "-framerate",
+ "1",
+ "-i",
+ str(FRAMES / "frame-%03d.png"),
+ "-vf",
+ "scale=920:520:flags=lanczos",
+ str(gif_path),
+ ],
+ check=True,
+)
+
+subprocess.run(
+ [
+ ffmpeg,
+ "-y",
+ "-framerate",
+ "1",
+ "-i",
+ str(FRAMES / "frame-%03d.png"),
+ "-vf",
+ "scale=920:520:flags=lanczos,format=yuv420p",
+ "-movflags",
+ "+faststart",
+ str(mp4_path),
+ ],
+ check=True,
+)
+
+print(f"wrote {gif_path}")
+print(f"wrote {mp4_path}")
diff --git a/sponsor-escrow-readiness-guard/index.js b/sponsor-escrow-readiness-guard/index.js
new file mode 100644
index 00000000..ade59a78
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/index.js
@@ -0,0 +1,207 @@
+const DAY_MS = 24 * 60 * 60 * 1000;
+const SUPPORTED_CURRENCIES = new Set(["USD", "EUR", "GBP"]);
+
+function parseDate(value, field) {
+ const date = new Date(`${value}T00:00:00Z`);
+ if (Number.isNaN(date.getTime())) {
+ throw new Error(`Invalid date for ${field}: ${value}`);
+ }
+ return date;
+}
+
+function daysBetween(a, b) {
+ return Math.floor((b.getTime() - a.getTime()) / DAY_MS);
+}
+
+function moneyAmount(value, field) {
+ const amount = Number(value);
+ if (!Number.isFinite(amount) || amount < 0) {
+ throw new Error(`${field} must be a non-negative number.`);
+ }
+ return Math.round(amount * 100) / 100;
+}
+
+function buildFinding(code, severity, message, remediation) {
+ return { code, severity, message, remediation };
+}
+
+function payoutScheduleTotal(schedule) {
+ return (schedule || []).reduce((sum, item) => sum + moneyAmount(item.amount, "payout.amount"), 0);
+}
+
+function validatePayoutSchedule(challenge) {
+ const findings = [];
+ const schedule = challenge.payoutSchedule || [];
+ const prizeAmount = moneyAmount(challenge.prizeAmount, "challenge.prizeAmount");
+
+ if (schedule.length === 0) {
+ findings.push(buildFinding(
+ "PAYOUT_SCHEDULE_MISSING",
+ "block",
+ `${challenge.id} does not define milestone or final payout routing.`,
+ "Attach a payout schedule before solver submissions open."
+ ));
+ return findings;
+ }
+
+ const total = payoutScheduleTotal(schedule);
+ if (total !== prizeAmount) {
+ findings.push(buildFinding(
+ "PAYOUT_TOTAL_MISMATCH",
+ "block",
+ `${challenge.id} payout schedule totals ${total}, but advertised prize is ${prizeAmount}.`,
+ "Reconcile milestone and final payout rows to the exact advertised prize."
+ ));
+ }
+
+ for (const row of schedule) {
+ if (!row.releaseTrigger || !row.destinationClass) {
+ findings.push(buildFinding(
+ "PAYOUT_ROUTE_INCOMPLETE",
+ "revise",
+ `${challenge.id} has a payout row without release trigger or destination class.`,
+ "Add release trigger and solver/team/institution destination class to each payout row."
+ ));
+ }
+ }
+
+ return findings;
+}
+
+function validateEscrowEvidence(challenge, now) {
+ const findings = [];
+ const escrow = challenge.escrow || {};
+ const prizeAmount = moneyAmount(challenge.prizeAmount, "challenge.prizeAmount");
+ const awardDate = parseDate(challenge.awardDecisionDate, "challenge.awardDecisionDate");
+
+ if (!SUPPORTED_CURRENCIES.has(challenge.currency)) {
+ findings.push(buildFinding(
+ "UNSUPPORTED_CURRENCY",
+ "block",
+ `${challenge.id} uses unsupported prize currency ${challenge.currency}.`,
+ "Convert the challenge prize to a supported payout currency before launch."
+ ));
+ }
+
+ if (!escrow.provider || !escrow.evidenceId) {
+ findings.push(buildFinding(
+ "ESCROW_EVIDENCE_MISSING",
+ "block",
+ `${challenge.id} lacks sponsor escrow evidence.`,
+ "Attach escrow provider and evidence ID before accepting submissions."
+ ));
+ }
+
+ const fundedAmount = moneyAmount(escrow.fundedAmount || 0, "escrow.fundedAmount");
+ if (fundedAmount < prizeAmount) {
+ findings.push(buildFinding(
+ "ESCROW_UNDERFUNDED",
+ "block",
+ `${challenge.id} escrow holds ${fundedAmount}, below advertised prize ${prizeAmount}.`,
+ "Fund the escrow to at least the advertised prize before challenge launch."
+ ));
+ }
+
+ if (!escrow.authorizedBy || !escrow.releaseApprovalId) {
+ findings.push(buildFinding(
+ "SPONSOR_RELEASE_AUTH_MISSING",
+ "block",
+ `${challenge.id} has no sponsor release authorization on file.`,
+ "Record sponsor release approval before the prize is advertised as payable."
+ ));
+ }
+
+ if (escrow.expiresAt) {
+ const expiresAt = parseDate(escrow.expiresAt, "escrow.expiresAt");
+ if (expiresAt < awardDate) {
+ findings.push(buildFinding(
+ "ESCROW_EXPIRES_BEFORE_AWARD",
+ "block",
+ `${challenge.id} escrow expires before the award decision date.`,
+ "Extend escrow validity beyond the award decision and dispute window."
+ ));
+ }
+ }
+
+ if (escrow.verifiedAt) {
+ const verifiedAt = parseDate(escrow.verifiedAt, "escrow.verifiedAt");
+ if (daysBetween(verifiedAt, now) > 30) {
+ findings.push(buildFinding(
+ "ESCROW_EVIDENCE_STALE",
+ "revise",
+ `${challenge.id} escrow verification is older than 30 days.`,
+ "Refresh escrow evidence before opening or continuing submissions."
+ ));
+ }
+ }
+
+ return findings;
+}
+
+function summarizeDecision(findings) {
+ if (findings.some((item) => item.severity === "block")) {
+ return "hold";
+ }
+ if (findings.some((item) => item.severity === "revise")) {
+ return "revise";
+ }
+ return "release";
+}
+
+function evaluateSponsorEscrowPacket(packet) {
+ if (!packet || typeof packet !== "object") {
+ throw new Error("Packet must be an object.");
+ }
+ if (!Array.isArray(packet.challenges)) {
+ throw new Error("Packet requires challenges.");
+ }
+
+ const now = new Date(packet.generatedAt || Date.now());
+ if (Number.isNaN(now.getTime())) {
+ throw new Error("generatedAt must be a valid timestamp.");
+ }
+
+ const challengeResults = packet.challenges.map((challenge) => {
+ const findings = [
+ ...validateEscrowEvidence(challenge, now),
+ ...validatePayoutSchedule(challenge)
+ ];
+
+ return {
+ challengeId: challenge.id,
+ sponsorId: challenge.sponsorId,
+ prizeAmount: moneyAmount(challenge.prizeAmount, "challenge.prizeAmount"),
+ currency: challenge.currency,
+ decision: summarizeDecision(findings),
+ blockerCount: findings.filter((item) => item.severity === "block").length,
+ revisionCount: findings.filter((item) => item.severity === "revise").length,
+ findings
+ };
+ });
+
+ const allFindings = challengeResults.flatMap((item) => item.findings);
+ const decision = summarizeDecision(allFindings);
+
+ return {
+ batchId: packet.batchId,
+ generatedAt: now.toISOString(),
+ decision,
+ checkedChallenges: challengeResults.length,
+ releaseCount: challengeResults.filter((item) => item.decision === "release").length,
+ reviseCount: challengeResults.filter((item) => item.decision === "revise").length,
+ holdCount: challengeResults.filter((item) => item.decision === "hold").length,
+ blockerCount: allFindings.filter((item) => item.severity === "block").length,
+ revisionCount: allFindings.filter((item) => item.severity === "revise").length,
+ challengeResults,
+ reviewerSummary: decision === "release"
+ ? "All sponsor escrow and payout readiness checks are satisfied."
+ : "Hold or revise challenge launch until prize funding evidence is complete."
+ };
+}
+
+module.exports = {
+ SUPPORTED_CURRENCIES,
+ evaluateSponsorEscrowPacket,
+ payoutScheduleTotal,
+ summarizeDecision
+};
diff --git a/sponsor-escrow-readiness-guard/package.json b/sponsor-escrow-readiness-guard/package.json
new file mode 100644
index 00000000..17f4b44c
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "sponsor-escrow-readiness-guard",
+ "version": "1.0.0",
+ "description": "Sponsor escrow and prize funding readiness guard for the SCIBASE Scientific Bounty System",
+ "main": "index.js",
+ "type": "commonjs",
+ "scripts": {
+ "test": "node test.js",
+ "demo": "node demo.js",
+ "demo:video": "python demo_video.py"
+ },
+ "license": "MIT"
+}
diff --git a/sponsor-escrow-readiness-guard/reports/demo.gif b/sponsor-escrow-readiness-guard/reports/demo.gif
new file mode 100644
index 00000000..651d260b
Binary files /dev/null and b/sponsor-escrow-readiness-guard/reports/demo.gif differ
diff --git a/sponsor-escrow-readiness-guard/reports/demo.mp4 b/sponsor-escrow-readiness-guard/reports/demo.mp4
new file mode 100644
index 00000000..7de2c561
Binary files /dev/null and b/sponsor-escrow-readiness-guard/reports/demo.mp4 differ
diff --git a/sponsor-escrow-readiness-guard/reports/escrow-readiness-report.json b/sponsor-escrow-readiness-guard/reports/escrow-readiness-report.json
new file mode 100644
index 00000000..8a4d7ce7
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/reports/escrow-readiness-report.json
@@ -0,0 +1,118 @@
+{
+ "generatedAt": "2026-06-02T00:00:00.000Z",
+ "module": "sponsor-escrow-readiness-guard",
+ "clean": {
+ "batchId": "escrow-batch-clean",
+ "generatedAt": "2026-06-02T00:00:00.000Z",
+ "decision": "release",
+ "checkedChallenges": 1,
+ "releaseCount": 1,
+ "reviseCount": 0,
+ "holdCount": 0,
+ "blockerCount": 0,
+ "revisionCount": 0,
+ "challengeResults": [
+ {
+ "challengeId": "challenge-biomarker-001",
+ "sponsorId": "pharma-lab-alpha",
+ "prizeAmount": 150000,
+ "currency": "USD",
+ "decision": "release",
+ "blockerCount": 0,
+ "revisionCount": 0,
+ "findings": []
+ }
+ ],
+ "reviewerSummary": "All sponsor escrow and payout readiness checks are satisfied."
+ },
+ "risky": {
+ "batchId": "escrow-batch-risky",
+ "generatedAt": "2026-06-02T00:00:00.000Z",
+ "decision": "hold",
+ "checkedChallenges": 2,
+ "releaseCount": 0,
+ "reviseCount": 0,
+ "holdCount": 2,
+ "blockerCount": 7,
+ "revisionCount": 2,
+ "challengeResults": [
+ {
+ "challengeId": "challenge-quantum-042",
+ "sponsorId": "quantum-startup-beta",
+ "prizeAmount": 80000,
+ "currency": "USD",
+ "decision": "hold",
+ "blockerCount": 5,
+ "revisionCount": 2,
+ "findings": [
+ {
+ "code": "ESCROW_EVIDENCE_MISSING",
+ "severity": "block",
+ "message": "challenge-quantum-042 lacks sponsor escrow evidence.",
+ "remediation": "Attach escrow provider and evidence ID before accepting submissions."
+ },
+ {
+ "code": "ESCROW_UNDERFUNDED",
+ "severity": "block",
+ "message": "challenge-quantum-042 escrow holds 42000, below advertised prize 80000.",
+ "remediation": "Fund the escrow to at least the advertised prize before challenge launch."
+ },
+ {
+ "code": "SPONSOR_RELEASE_AUTH_MISSING",
+ "severity": "block",
+ "message": "challenge-quantum-042 has no sponsor release authorization on file.",
+ "remediation": "Record sponsor release approval before the prize is advertised as payable."
+ },
+ {
+ "code": "ESCROW_EXPIRES_BEFORE_AWARD",
+ "severity": "block",
+ "message": "challenge-quantum-042 escrow expires before the award decision date.",
+ "remediation": "Extend escrow validity beyond the award decision and dispute window."
+ },
+ {
+ "code": "ESCROW_EVIDENCE_STALE",
+ "severity": "revise",
+ "message": "challenge-quantum-042 escrow verification is older than 30 days.",
+ "remediation": "Refresh escrow evidence before opening or continuing submissions."
+ },
+ {
+ "code": "PAYOUT_TOTAL_MISMATCH",
+ "severity": "block",
+ "message": "challenge-quantum-042 payout schedule totals 60000, but advertised prize is 80000.",
+ "remediation": "Reconcile milestone and final payout rows to the exact advertised prize."
+ },
+ {
+ "code": "PAYOUT_ROUTE_INCOMPLETE",
+ "severity": "revise",
+ "message": "challenge-quantum-042 has a payout row without release trigger or destination class.",
+ "remediation": "Add release trigger and solver/team/institution destination class to each payout row."
+ }
+ ]
+ },
+ {
+ "challengeId": "challenge-climate-forecast-017",
+ "sponsorId": "climate-nonprofit-gamma",
+ "prizeAmount": 25000,
+ "currency": "POINTS",
+ "decision": "hold",
+ "blockerCount": 2,
+ "revisionCount": 0,
+ "findings": [
+ {
+ "code": "UNSUPPORTED_CURRENCY",
+ "severity": "block",
+ "message": "challenge-climate-forecast-017 uses unsupported prize currency POINTS.",
+ "remediation": "Convert the challenge prize to a supported payout currency before launch."
+ },
+ {
+ "code": "PAYOUT_SCHEDULE_MISSING",
+ "severity": "block",
+ "message": "challenge-climate-forecast-017 does not define milestone or final payout routing.",
+ "remediation": "Attach a payout schedule before solver submissions open."
+ }
+ ]
+ }
+ ],
+ "reviewerSummary": "Hold or revise challenge launch until prize funding evidence is complete."
+ }
+}
\ No newline at end of file
diff --git a/sponsor-escrow-readiness-guard/reports/escrow-readiness-report.md b/sponsor-escrow-readiness-guard/reports/escrow-readiness-report.md
new file mode 100644
index 00000000..e35149cc
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/reports/escrow-readiness-report.md
@@ -0,0 +1,36 @@
+# Sponsor Escrow Readiness Report
+
+Generated: 2026-06-02T00:00:00.000Z
+
+## Clean Packet
+
+Decision: release
+
+### challenge-biomarker-001
+
+Decision: release
+
+- No findings.
+
+## Risky Packet
+
+Decision: hold
+
+### challenge-quantum-042
+
+Decision: hold
+
+- BLOCK ESCROW_EVIDENCE_MISSING: challenge-quantum-042 lacks sponsor escrow evidence. Attach escrow provider and evidence ID before accepting submissions.
+- BLOCK ESCROW_UNDERFUNDED: challenge-quantum-042 escrow holds 42000, below advertised prize 80000. Fund the escrow to at least the advertised prize before challenge launch.
+- BLOCK SPONSOR_RELEASE_AUTH_MISSING: challenge-quantum-042 has no sponsor release authorization on file. Record sponsor release approval before the prize is advertised as payable.
+- BLOCK ESCROW_EXPIRES_BEFORE_AWARD: challenge-quantum-042 escrow expires before the award decision date. Extend escrow validity beyond the award decision and dispute window.
+- REVISE ESCROW_EVIDENCE_STALE: challenge-quantum-042 escrow verification is older than 30 days. Refresh escrow evidence before opening or continuing submissions.
+- BLOCK PAYOUT_TOTAL_MISMATCH: challenge-quantum-042 payout schedule totals 60000, but advertised prize is 80000. Reconcile milestone and final payout rows to the exact advertised prize.
+- REVISE PAYOUT_ROUTE_INCOMPLETE: challenge-quantum-042 has a payout row without release trigger or destination class. Add release trigger and solver/team/institution destination class to each payout row.
+
+### challenge-climate-forecast-017
+
+Decision: hold
+
+- BLOCK UNSUPPORTED_CURRENCY: challenge-climate-forecast-017 uses unsupported prize currency POINTS. Convert the challenge prize to a supported payout currency before launch.
+- BLOCK PAYOUT_SCHEDULE_MISSING: challenge-climate-forecast-017 does not define milestone or final payout routing. Attach a payout schedule before solver submissions open.
diff --git a/sponsor-escrow-readiness-guard/reports/frames/frame-000.png b/sponsor-escrow-readiness-guard/reports/frames/frame-000.png
new file mode 100644
index 00000000..fd0184f8
Binary files /dev/null and b/sponsor-escrow-readiness-guard/reports/frames/frame-000.png differ
diff --git a/sponsor-escrow-readiness-guard/reports/frames/frame-001.png b/sponsor-escrow-readiness-guard/reports/frames/frame-001.png
new file mode 100644
index 00000000..3e00b106
Binary files /dev/null and b/sponsor-escrow-readiness-guard/reports/frames/frame-001.png differ
diff --git a/sponsor-escrow-readiness-guard/reports/frames/frame-002.png b/sponsor-escrow-readiness-guard/reports/frames/frame-002.png
new file mode 100644
index 00000000..fb9261cf
Binary files /dev/null and b/sponsor-escrow-readiness-guard/reports/frames/frame-002.png differ
diff --git a/sponsor-escrow-readiness-guard/reports/summary.svg b/sponsor-escrow-readiness-guard/reports/summary.svg
new file mode 100644
index 00000000..01ae43b6
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/reports/summary.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/sponsor-escrow-readiness-guard/requirements-map.md b/sponsor-escrow-readiness-guard/requirements-map.md
new file mode 100644
index 00000000..ad98ae40
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/requirements-map.md
@@ -0,0 +1,24 @@
+# Requirements Map
+
+This contribution maps to SCIBASE issue #18, Scientific Bounty System.
+
+## Arbitration & Reward Distribution
+
+- Escrowed prize funds: verifies that challenge prize funds are present before submissions proceed.
+- Partial payments for milestones: reconciles milestone payout schedules against the advertised prize.
+- Payout routing: checks that each payout entry has a destination class and release trigger.
+- Solver trust: blocks challenge launch when sponsor funding or release authorization is incomplete.
+
+## Challenge Posting Portal
+
+- Prize amount and payout schedule: validates exact schedule totals, currency, and deadlines.
+- Timeline and milestone deadlines: checks escrow validity through the award decision window.
+
+## Submission Engine
+
+- Submission protection: produces hold/revise decisions before solvers spend time on an underfunded challenge.
+- Audit logs for reproducibility: emits deterministic JSON and Markdown reviewer artifacts.
+
+## Boundaries
+
+This is not a live payment integration. It is a synthetic, dependency-free guard that demonstrates how SCIBASE can prevent unfunded scientific bounties from entering the active solver marketplace.
diff --git a/sponsor-escrow-readiness-guard/sample-data.js b/sponsor-escrow-readiness-guard/sample-data.js
new file mode 100644
index 00000000..6fbdf1f6
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/sample-data.js
@@ -0,0 +1,92 @@
+const cleanPacket = {
+ batchId: "escrow-batch-clean",
+ generatedAt: "2026-06-02",
+ challenges: [
+ {
+ id: "challenge-biomarker-001",
+ sponsorId: "pharma-lab-alpha",
+ currency: "USD",
+ prizeAmount: 150000,
+ awardDecisionDate: "2026-09-30",
+ escrow: {
+ provider: "institutional-escrow",
+ evidenceId: "ESCROW-2026-001",
+ fundedAmount: 150000,
+ verifiedAt: "2026-05-28",
+ expiresAt: "2026-10-31",
+ authorizedBy: "sponsor-cfo",
+ releaseApprovalId: "REL-001"
+ },
+ payoutSchedule: [
+ {
+ name: "prototype milestone",
+ amount: 50000,
+ releaseTrigger: "prototype accepted",
+ destinationClass: "solver-team"
+ },
+ {
+ name: "final award",
+ amount: 100000,
+ releaseTrigger: "final evaluation complete",
+ destinationClass: "solver-team"
+ }
+ ]
+ }
+ ]
+};
+
+const riskyPacket = {
+ batchId: "escrow-batch-risky",
+ generatedAt: "2026-06-02",
+ challenges: [
+ {
+ id: "challenge-quantum-042",
+ sponsorId: "quantum-startup-beta",
+ currency: "USD",
+ prizeAmount: 80000,
+ awardDecisionDate: "2026-08-15",
+ escrow: {
+ provider: "",
+ evidenceId: "",
+ fundedAmount: 42000,
+ verifiedAt: "2026-03-15",
+ expiresAt: "2026-08-01",
+ authorizedBy: "",
+ releaseApprovalId: ""
+ },
+ payoutSchedule: [
+ {
+ name: "proposal winner",
+ amount: 10000,
+ releaseTrigger: "proposal accepted",
+ destinationClass: "solver-team"
+ },
+ {
+ name: "final winner",
+ amount: 50000,
+ releaseTrigger: "",
+ destinationClass: ""
+ }
+ ]
+ },
+ {
+ id: "challenge-climate-forecast-017",
+ sponsorId: "climate-nonprofit-gamma",
+ currency: "POINTS",
+ prizeAmount: 25000,
+ awardDecisionDate: "2026-07-15",
+ escrow: {
+ provider: "institutional-escrow",
+ evidenceId: "ESCROW-2026-017",
+ fundedAmount: 25000,
+ verifiedAt: "2026-05-20",
+ expiresAt: "2026-08-31",
+ authorizedBy: "grant-admin",
+ releaseApprovalId: "REL-017"
+ },
+ payoutSchedule: []
+ }
+ ]
+};
+
+module.exports = { cleanPacket, riskyPacket };
diff --git a/sponsor-escrow-readiness-guard/test.js b/sponsor-escrow-readiness-guard/test.js
new file mode 100644
index 00000000..acb56949
--- /dev/null
+++ b/sponsor-escrow-readiness-guard/test.js
@@ -0,0 +1,39 @@
+const assert = require("node:assert/strict");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+const {
+ evaluateSponsorEscrowPacket,
+ payoutScheduleTotal,
+ summarizeDecision
+} = require("./index");
+
+const clean = evaluateSponsorEscrowPacket(cleanPacket);
+assert.equal(clean.decision, "release");
+assert.equal(clean.releaseCount, 1);
+assert.equal(clean.holdCount, 0);
+assert.equal(clean.blockerCount, 0);
+
+const risky = evaluateSponsorEscrowPacket(riskyPacket);
+assert.equal(risky.decision, "hold");
+assert.equal(risky.checkedChallenges, 2);
+assert.equal(risky.holdCount, 2);
+assert.equal(risky.blockerCount, 7);
+assert.equal(risky.revisionCount, 2);
+
+const riskyCodes = risky.challengeResults.flatMap((item) => item.findings.map((finding) => finding.code));
+assert.ok(riskyCodes.includes("ESCROW_EVIDENCE_MISSING"));
+assert.ok(riskyCodes.includes("ESCROW_UNDERFUNDED"));
+assert.ok(riskyCodes.includes("SPONSOR_RELEASE_AUTH_MISSING"));
+assert.ok(riskyCodes.includes("ESCROW_EXPIRES_BEFORE_AWARD"));
+assert.ok(riskyCodes.includes("ESCROW_EVIDENCE_STALE"));
+assert.ok(riskyCodes.includes("PAYOUT_TOTAL_MISMATCH"));
+assert.ok(riskyCodes.includes("PAYOUT_ROUTE_INCOMPLETE"));
+assert.ok(riskyCodes.includes("UNSUPPORTED_CURRENCY"));
+assert.ok(riskyCodes.includes("PAYOUT_SCHEDULE_MISSING"));
+
+assert.equal(payoutScheduleTotal(cleanPacket.challenges[0].payoutSchedule), 150000);
+assert.equal(summarizeDecision([{ severity: "revise" }]), "revise");
+assert.equal(summarizeDecision([{ severity: "block" }, { severity: "revise" }]), "hold");
+assert.throws(() => evaluateSponsorEscrowPacket({ generatedAt: "not-a-date", challenges: [] }));
+assert.throws(() => evaluateSponsorEscrowPacket({ generatedAt: "2026-06-02" }));
+
+console.log("sponsor escrow readiness guard tests passed");