Escrow that checks the work before it pays.
Clients and creators negotiate commissions in messy Discord threads and emails. CommishGuard AI turns that thread into a strict, machine-checkable contract, holds the client's money in a PayPal Sandbox authorization, inspects the creator's actual deliverable with deterministic Java, and captures the payment only when the file passes.
AI is for ambiguity. Code is for truth.
The LLM reads the chat. Java decides whether the.jaris real.
Digital commissions run on trust and screenshots:
- The client pays first and hopes the file arrives.
- The creator delivers first and hopes the client pays.
- "It's a 4K PNG" is usually a claim, not a fact.
- Chargebacks and "the file was broken" disputes have no shared evidence.
CommishGuard makes the deal itself executable. The price, file type, resolution, and required entries are agreed up front, the money is held (not captured), and the release decision is made by code that can be audited line by line.
flowchart LR
A["Deal chat<br/>Discord · email"] -->|AI parse + validation| B["Strict contract<br/>price · extension · checks"]
B -->|AUTHORIZE| C["PayPal Sandbox<br/>order created"]
C -->|buyer approves| D["Authorization hold<br/>funds not captured"]
E["Deliverable<br/>.jar · .zip · .png"] --> F["FileVerifier<br/>deterministic Java"]
F -->|pass| G["Capture<br/>creator paid"]
D --> G
F -->|fail| H["Blocked<br/>funds stay held"]
- Paste the deal chat. The parser extracts a contract and the server re-validates every field.
- Hold the money. The server creates a PayPal order with
intent=AUTHORIZE. The buyer approves; funds are authorized, not captured. - Upload the deliverable.
.jar,.zip, or.pngis inspected in a quarantine directory. It is never executed. - Release or block. A passing verification triggers capture automatically. A failing one keeps the money held and shows every failed check.
A ledger persisted to storage/ledger.json records each state change and provides the AG Grid dashboard. When a payment is captured, InvoiceService renders a two-page PDF receipt with the contract, verification evidence, PayPal IDs, and the latest audit events.
| Contract | Blocked | Paid |
|---|---|---|
![]() |
![]() |
![]() |
| Terms extracted and validated | Missing classes stopped before money moves | Every check and PayPal event retained |
The invoice is generated from the same ledger record that drove the capture. It includes the contract terms, every deterministic check, the PayPal order/authorization/capture IDs, and the latest six audit events. Mock and sample invoices carry a visible SANDBOX / DEMO footer.
Prerequisite: JDK 21. The Gradle wrapper downloads the correct Gradle version. AG Grid loads from jsDelivr, so first paint needs network access.
cp .env.example .env # optional — everything works without it
./gradlew run --console=plainWindows PowerShell:
Copy-Item .env.example .env
.\gradlew.bat run --console=plainOpen http://localhost:7070.
Zero-config mock — default. Leave .env empty. PAYPAL_MOCK auto-enables, the deterministic parser runs, no AI call is made, and no real money moves. The top bar shows a quiet PAYPAL MOCK label.
Live Sandbox + LLM. Add the three required keys and restart. For a non-OpenAI provider, also set its endpoint and model:
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
AI_API_KEY=...
# Gemini example (verified):
LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
LLM_MODEL=gemini-3.5-flashThe app now uses Environment.SANDBOX and the live LLM parser. The top bar changes to a muted SANDBOX LIVE pill, and the parser badge shows the model name. LLM_API_KEY remains accepted as an alias for AI_API_KEY; PAYPAL_MOCK=true still forces mock mode.
- Click Sample → Parse contract → Hold funds → Fund escrow ↗.
- In mock mode, approve on the local mock checkout. In live mode, sign in with a PayPal Sandbox personal account.
- Drag
demo-assets/broken-submission.jaronto the dropzone. It is blocked; open the row to see exactly which checks failed. - Drag
demo-assets/verified-motionblur-1.21.jar. All checks pass, capture fires automatically, and the row becomes Paid out. - Open the Paid out row and choose Download Invoice PDF to see the generated receipt.
Regenerate the payloads at any time with ./gradlew generateDemoAssets. The seeded ledger ships with three clearly labelled SAMPLE rows so the dashboard is never empty on first paint. They are not real transactions.
Use Edit deal chat on the contract card to bring the original thread back at any time. demo/aetheris-1920.png and demo/fake.png still exercise the image checks.
Implemented with the official PayPal Server SDK (com.paypal.sdk:paypal-server-sdk:0.6.1), not hand-rolled REST.
| Step | SDK call | Result |
|---|---|---|
| Create order | OrdersController.ordersCreate with CheckoutPaymentIntent.AUTHORIZE |
Order id + buyer approval link |
| Buyer approves | PayPal-hosted checkout, return to /api/paypal/return |
Order APPROVED |
| Place hold | OrdersController.ordersAuthorize |
Authorization id; funds held |
| Release | PaymentsController.authorizationsCapture |
Capture id; payment taken |
| Fallback | OrdersController.ordersCapture |
Used only if order-level capture is required |
| Cancel | PaymentsController.authorizationsVoid |
Hold released where possible |
Guardrails:
Environment.SANDBOXis forced. Live credentials are never used by this build.- Order creation sends a
PayPal-Request-Ididempotency header derived from the commission. - The return endpoint calls
ordersAuthorizeand persists the authorization id; refreshing it reuses the hold instead of creating a second one. - Capture sends
final_capture=true, closing the authorization rather than leaving it open. - Amounts are normalized to 2 decimals in USD; the amount captured is the validated contract amount, never a client-supplied value.
- Client id/secret stay server-side. The browser only receives the approval URL.
- Mock mode returns
MOCK-…ids and a local checkout page so the flow is fully demoable offline.
Relevant code: PayPalService.java.
# .env
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
AI_API_KEY=... # optional: enables the live parser./gradlew paypalSmoke --console=plainThe task prints the Sandbox order id and approval URL, or skips cleanly when credentials are absent. On restart, the top bar switches from PayPal mock to the outlined Sandbox Live pill.
AiContractService calls any OpenAI-compatible chat-completions endpoint using Java 21 HttpClient.
- System prompt locks the output to JSON only and defines the exact schema.
- Temperature
0.1;response_format: json_objectis requested where supported. - Fenced JSON and surrounding prose are stripped defensively.
- The result is always re-validated server-side: price range, extension whitelist, checklist size, title length.
- Chat text is treated as untrusted data, never as model instructions. A model that emits extra fields cannot widen the contract.
AI_API_KEYenables the live parser (LLM_API_KEYis accepted as an alias); the endpoint, model, and timeout are configurable.- If no key is configured — or the call fails — a deterministic regex parser handles
$240,240 USD,.jar,4k,1920x1080,1080p, and entry names such asfabric.mod.json.
Contract shape:
{
"title": "Vex Armor — Fabric 1.21 mod",
"priceUSD": 240.00,
"requiredExtension": ".jar",
"minResolutionOrEntryCheck": "fabric.mod.json",
"checklist": [
"Fabric mod loads on 1.21.x",
"Custom armor renders with emissive trim",
"Source files included"
]
}Relevant code: AiContractService.java.
FileVerifier trusts bytes, never filenames or Content-Type.
| Check | .jar / .zip |
.png |
|---|---|---|
| Extension matches contract | ✓ | ✓ |
| Magic bytes / real container | ZIP PK signature |
89 50 4E 47 signature |
| Non-empty, under size limit | ✓ | ✓ |
| Parses as valid archive / image | ZipInputStream walk |
ImageIO.read non-null |
| Required content | ≥1 compiled .class with CAFEBABE; fabric.mod.json when required; manifest warns only |
Width/height meet agreed minimum |
| Anti-fake | duplicate entry detection | rejects blank / single-color canvases |
| Abuse limits | max 5,000 entries, 250 MB uncompressed, 200:1 ratio | min 400 rendered pixels |
| Path safety | rejects .., absolute paths, backslash traversal |
— |
Every check returns a label, pass/fail, and a human-readable reason shown in the drawer. Uploads land in storage/uploads/<commission>/ with a UUID filename; the original name is display-only.
Relevant code: FileVerifier.java.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
Version, PayPal mode, parser mode, uptime |
POST |
/api/contracts/parse |
{chat} → {contract, source, warnings} |
GET |
/api/commissions |
Ledger rows + aggregate stats |
POST |
/api/commissions |
{chat, contract} → create order and hold |
GET |
/api/commissions/{id} |
One commission with evidence |
POST |
/api/commissions/{id}/deliverable |
Multipart file → verify → auto-capture |
POST |
/api/commissions/{id}/release |
Manual capture, verified rows only |
GET |
/api/commissions/{id}/invoice |
Download the PDF receipt for a RELEASED commission |
POST |
/api/commissions/{id}/cancel |
Void authorization / cancel commission |
GET |
/api/paypal/return |
Buyer approval return URL |
GET |
/api/paypal/cancel |
Buyer cancellation return URL |
GET |
/api/paypal/mock-checkout |
Mock approval page (mock mode only) |
GET |
/api/paypal/mock-approve |
Finish mock authorization |
Errors use a stable shape: {"error": "...", "code": "..."}. Secret values and raw stack traces never reach the browser.
Copy .env.example to .env; it is git-ignored.
| Variable | Default | Notes |
|---|---|---|
PORT |
7070 |
HTTP port |
PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET |
— | Sandbox app credentials only |
PAYPAL_MOCK |
auto | true forces mock; absent credentials auto-enable mock |
PAYPAL_RETURN_URL / PAYPAL_CANCEL_URL |
localhost URLs | Must match the Sandbox app settings |
AI_API_KEY |
— | Enables the AI parser; preferred name. LLM_API_KEY is an alias |
LLM_BASE_URL |
https://api.openai.com/v1 |
Any OpenAI-compatible endpoint |
LLM_MODEL |
gpt-4o-mini |
Model id |
LLM_TIMEOUT_SECONDS |
20 |
HTTP timeout |
FILE_MAX_MB |
50 |
Per-upload limit |
STORAGE_DIR |
storage |
Ledger + quarantine directory |
VERIFY_MAX_ENTRIES |
5000 |
Zip entry cap |
VERIFY_MAX_UNCOMPRESSED_MB |
250 |
Zip-bomb guard |
VERIFY_MAX_RATIO |
200 |
Compression-ratio cap |
VERIFY_MIN_RENDER_PIXELS |
400 |
Blank-image guard |
./gradlew test --console=plain # unit + integration tests
./gradlew build --console=plain # tests + distribution
./gradlew installDist # runnable build/install/commishguard/
./gradlew generateDemoAssets # rebuild the two video payloadsCoverage includes contract validation, fallback parsing, fenced/invalid LLM output, PNG/JAR acceptance and rejection, zip-slip and zip-bomb defenses, mock PayPal lifecycle, live Sandbox mode selection, PDF invoice rendering with font-safe text, and a sandbox smoke task.
src/main/java/com/commishguard/
├── App.java Javalin bootstrap, port 7070
├── config/EnvConfig.java typed env + .env loading
├── model/ Contract, Commission, VerificationResult, audit events
├── service/
│ ├── AiContractService.java LLM call + deterministic fallback + validation
│ ├── PayPalService.java Sandbox AUTHORIZE → authorize → capture, mock mode
│ ├── FileVerifier.java archive / image inspection
│ ├── InvoiceService.java OpenPDF receipt for RELEASED commissions
│ └── LedgerService.java state machine + atomic JSON persistence
└── web/Api.java routes, multipart, error mapping
src/main/resources/public/index.html single-page studio + AG Grid ledger
docs/ screenshots used in this README
demo-assets/ reproducible video payloads (pass / block)
demo/ legacy fixtures: real/fake PNG + small JARs
storage/ ledger.json + quarantined uploads (git-ignored)
- PayPal Server SDK — real Sandbox
AUTHORIZEorder lifecycle, authorization hold, capture, void, idempotency. - AG Grid Community 32.3.3 — the primary ledger: sortable/resizable columns, custom status and verification cell renderers, live polling, row-click evidence drawer.
- AI — load-bearing contract extraction with server-side validation and a deterministic fallback.
Architecture and failure-mode detail: ARCHITECTURE.md. Execution ledger: TODO.md.
- No secrets in the repo;
.envis ignored,.env.exampledocuments every knob. - Uploads are treated as hostile: quarantine names, magic-byte checks, traversal rejection, decompression limits. Nothing is executed or shelled out.
- The browser is untrusted; price, verification, and capture decisions are recomputed server-side.
- Sandbox only. Mock mode is visibly labelled so judges are never misled.
- Webhooks (
CHECKOUT.ORDER.APPROVED) would replace the polling return URL. - Single-file deliverables; bundles with per-item criteria are the next step.
- Ledger is JSON on disk; Postgres is the production path.
- Reputation scoring and additional payment rails are intentionally out of scope for the hackathon build.
MIT — see LICENSE. PDF rendering uses OpenPDF 2.0.3 (LGPL/MPL); review its license before redistributing the bundled distribution.




