Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions sponsor-escrow-readiness-guard/README.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions sponsor-escrow-readiness-guard/demo.js
Original file line number Diff line number Diff line change
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="920" height="520" viewBox="0 0 920 520">
<rect width="920" height="520" fill="#0f172a"/>
<rect x="60" y="56" width="800" height="408" rx="14" fill="#f8fafc"/>
<text x="104" y="128" font-family="Arial, sans-serif" font-size="34" font-weight="700" fill="#0f172a">Sponsor Escrow Readiness Guard</text>
<text x="104" y="190" font-family="Arial, sans-serif" font-size="24" fill="#334155">Release: ${clean.releaseCount} challenge</text>
<rect x="104" y="220" width="260" height="52" fill="#16a34a"/>
<text x="386" y="256" font-family="Arial, sans-serif" font-size="20" fill="#0f172a">Clean escrow and payout schedule</text>
<text x="104" y="330" font-family="Arial, sans-serif" font-size="24" fill="#334155">Hold: ${risky.holdCount} challenges</text>
<rect x="104" y="360" width="520" height="52" fill="#dc2626"/>
<text x="646" y="396" font-family="Arial, sans-serif" font-size="20" fill="#0f172a">${risky.blockerCount} blockers, ${risky.revisionCount} revisions</text>
</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")}`);
88 changes: 88 additions & 0 deletions sponsor-escrow-readiness-guard/demo_video.py
Original file line number Diff line number Diff line change
@@ -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}")
207 changes: 207 additions & 0 deletions sponsor-escrow-readiness-guard/index.js
Original file line number Diff line number Diff line change
@@ -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
};
Loading