Skip to content

Commit 57e0fac

Browse files
authored
Merge pull request #24 from ShieldTech-Ltd/feature/judge-demo-decision-receipt
feat: add judge-ready decision rehearsal demo
2 parents 953ab14 + a4950fd commit 57e0fac

10 files changed

Lines changed: 858 additions & 69 deletions

File tree

PITCH_STAGE_GUIDE.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
## 📋 Pre-Presentation Stage Checklist
1010

11-
- [x] **Live Vercel Deployment**: App live at `https://finpulse-ai.vercel.app` (or local backup `http://localhost:8080`).
12-
- [x] **Express REST API Server**: Node.js backend running on port 3000 with FCA Warning List DB.
11+
- [x] **Live Vercel Deployment**: App live at `https://finpulse-ai-one.vercel.app` (or local backup `http://localhost:3000`).
12+
- [x] **Express REST API Server**: Educational API with claim-pattern checks, decision rehearsal and receipt generation.
1313
- [x] **Stage Demo Preset Bar**: 1-Click presets ready for zero-friction stage scoring.
14-
- [x] **PWA Mobile Offline Readiness**: ServiceWorker `sw.js` cached for zero-latency presentation.
15-
- [x] **Stage QR Code**: Embedded on Slide 1 for instant judge phone scanning.
14+
- [ ] **Offline Backup**: Keep a tested local copy open before taking the stage.
15+
- [ ] **Stage QR Code**: Add the final production URL to Slide 1 after deployment.
1616

1717
---
1818

@@ -22,14 +22,14 @@
2222
| :--- | :--- | :--- | :--- |
2323
| **0:00 - 0:10** | Slide 1 (Title + QR Code) | Stage Entrance | Invite judges to scan the QR code on screen to open FinPulse AI live on their phones. |
2424
| **0:10 - 0:45** | Slide 2 (Maya Persona) | The Problem Hook | Tell the story of Maya, 21, opening her first payslip in London confused by tax codes, targeted by 50x leverage TikTok scams. |
25-
| **0:45 - 2:15** | **LIVE WORKING APP DEMO** | 90-Sec Demo | Click **`50x Forex Scam`** -> Show **95% RED FLAG SCORE** & FCA Warning List match. Show **Payslip Pension Match (+£70/mo)** and **5-Yr Compound Sandbox**. |
26-
| **2:15 - 3:15** | Slide 3 (B2B ROI & Metrics) | Business Model | Show Bank B2B Portal: **£1.42M FCA Fine Savings (18.9x ROI)** under FCA FG22/5 rules. |
25+
| **0:45 - 2:15** | **LIVE WORKING APP DEMO** | 90-Sec Demo | Select **50x Forex Scam** -> check claim-risk indicators -> enter Maya's £250 rental-deposit context -> reveal the 1% leverage downside -> answer the comprehension check -> generate the private Decision Receipt. |
26+
| **2:15 - 3:15** | Slide 3 (B2B Outcome Concept) | Business Model | Show how a partner could measure anonymous completion and comprehension outcomes. Clearly label the dashboard as a concept, not compliance proof. |
2727
| **3:15 - 4:00** | Slide 4 (Vision & Close) | The Close | *"Duolingo for Money + AI Anti-Scam Shield. Join us in building investors, not gamblers."* |
2828

2929
---
3030

3131
## ⚔️ Q&A Defense Quick Reference
3232

33-
1. **FCA Regulated Advice Boundary**: *"FinPulse AI provides financial capability education & scam math under FCA FG22/5 guidance, not regulated personal advice."*
34-
2. **Anti-Scraping Defense**: *"We use client-side OCR, user transcript submissions, and official platform API metadata endpoints — no fragile web scraping."*
35-
3. **Bank Buyer ROI**: *"Banks pay out of their FCA Consumer Duty compliance budget to satisfy legal mandates and prevent enforcement fines."*
33+
1. **FCA Regulated Advice Boundary**: *"FinPulse provides educational pattern checks and decision rehearsal, not financial advice, firm authorisation checks or scam determinations."*
34+
2. **Input Boundary**: *"For this prototype, the user pastes a claim or transcript. Real image and video OCR are intentionally outside the demo scope."*
35+
3. **Bank Buyer Hypothesis**: *"We would test whether banks, employers and universities will fund access as financial-capability infrastructure. Consumer Duty makes customer understanding relevant, but FinPulse does not itself prove compliance."*

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ FinPulse AI is an educational financial-literacy and scam-awareness prototype bu
55
It demonstrates:
66

77
- plain-language checks for common high-risk promotion patterns;
8+
- a guided pre-decision rehearsal with missing-context questions, a personalised downside scenario, a comprehension check and a private Decision Receipt;
89
- a simplified UK payslip learning calculator;
910
- hypothetical investment stress-test visualisations;
1011
- gamified financial-literacy lessons;
@@ -14,6 +15,16 @@ It demonstrates:
1415

1516
FinPulse AI does not provide financial advice, determine whether a firm is authorised, perform real image/video OCR, certify financial capability, or establish FCA Consumer Duty compliance. Outputs are educational prompts produced by deterministic rules. Verify firms directly through the official FCA Register and Warning List, and verify tax results against current HMRC guidance.
1617

18+
## 90-second judge demo
19+
20+
1. Select **50x Forex Scam** and run **Check Claim Risk Indicators**.
21+
2. Keep Maya's example amount and rental-deposit reason, then select **Rehearse Maya's Decision**.
22+
3. Show the missing context and personalised leverage downside.
23+
4. Answer **It magnifies losses as well as gains**.
24+
5. Generate the private Decision Receipt and close on the independent-verification next step.
25+
26+
The receipt records educational completion only. It intentionally stores neither the pasted claim nor Maya's personal reason.
27+
1728
The public deployment stores only short-lived, non-identifying demo events in memory. It intentionally has no durable customer database. Partner telemetry routes are hidden unless `ADMIN_API_TOKEN` is configured. The app does not mirror the FCA Warning List because its former RSS URL currently redirects to a missing page; users are sent to the official live list instead. A production service would additionally require authenticated users, tenant isolation, consent and retention controls, a durable managed datastore, monitoring, regulatory review, and independent security testing.
1829

1930
## Local development

app.js

Lines changed: 140 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
FinPulse AI - Commercial Platform Logic (Real Enterprise REST API Connected)
33
========================================================================== */
44

5-
const API_BASE_URL = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
6-
? 'http://localhost:3000/api/v1'
7-
: '/api/v1';
5+
const API_BASE_URL = '/api/v1';
86

97
document.addEventListener('DOMContentLoaded', () => {
108

@@ -158,8 +156,8 @@ document.addEventListener('DOMContentLoaded', () => {
158156
"The creator earns money from broker affiliate links when you lose your deposit.",
159157
"Unregulated offshore broker — no FCA protection or FSCS guarantee."
160158
],
161-
reality: "FCA data shows 82% of retail traders using forex leverage lose their entire initial deposit within 30 days. Real wealth comes from low-cost index compounding, not signal groups.",
162-
verdict: "DO NOT FOLLOW THIS ADVICE. High risk of complete capital destruction."
159+
reality: "Leverage magnifies losses as well as gains. At 100x leverage, a move of roughly 1% against a position could put the full amount at risk, before fees and platform rules.",
160+
verdict: "EDUCATIONAL CHECK: Verify the firm, evidence, costs and downside independently before deciding."
163161
},
164162
crypto: {
165163
text: "This new low-cap meme token is launching on DEX today! Guaranteed 100x gains before liquidity lock. Buy now or cry later! 💎🙌",
@@ -171,8 +169,8 @@ document.addEventListener('DOMContentLoaded', () => {
171169
"High creator token concentration: developers can dump holdings and drain liquidity pool.",
172170
"Fake hype generated by paid bot accounts on X and Telegram."
173171
],
174-
reality: "Over 97% of DEX meme tokens launched daily become inactive or worthless within 14 days. Treat crypto sandbox trading as high-risk speculation.",
175-
verdict: "HIGH RISK SPECULATION. Only gamble money you are 100% prepared to write off."
172+
reality: "A token promotion does not demonstrate liquidity, contract safety, ownership concentration or the ability to sell. Those facts require independent verification.",
173+
verdict: "EDUCATIONAL CHECK: Missing evidence is not proof of safety or fraud. Pause and verify independently."
176174
},
177175
bnpl: {
178176
text: "Why pay £120 for clothes today when you can split it into 4 easy interest-free payments? It's basically free money and doesn't affect anything! 🛍️✨",
@@ -184,8 +182,8 @@ document.addEventListener('DOMContentLoaded', () => {
184182
"Missing payments triggers missed payment markers reported to UK Credit Reference Agencies.",
185183
"Can severely impact your future mortgage or tenancy application suitability."
186184
],
187-
reality: "BNPL is a credit product, not free money. Missing a £15 payment can harm your ability to rent an apartment 2 years later.",
188-
verdict: "USE WITH CAUTION. Track all installments as real debt commitments."
185+
reality: "BNPL is a credit commitment, not free money. Several small instalments can overlap and reduce next month's available cash.",
186+
verdict: "EDUCATIONAL CHECK: Add every instalment to next month's committed spending before deciding."
189187
},
190188
index: {
191189
text: "Invest £150 a month into a low-cost, broadly diversified Global Index ETF (like S&P 500 or FTSE All-World) and let compound growth build long-term wealth over 10+ years.",
@@ -196,8 +194,8 @@ document.addEventListener('DOMContentLoaded', () => {
196194
"Short-term price fluctuations will occur during market dips.",
197195
"Requires long-term patience (5-10+ year time horizon)."
198196
],
199-
reality: "Historically, broad market index funds have delivered ~7-10% annualized returns over multi-decade periods with zero reliance on timing the market.",
200-
verdict: "SOLID FINANCIAL EDUCATION PRACTICE. Encourages investor mindset over gambling."
197+
reality: "Diversification can spread company-specific risk, but investment values can still fall and past performance does not guarantee future returns.",
198+
verdict: "EDUCATIONAL CHECK: Fees, time horizon, diversification and downside still require consideration."
201199
}
202200
};
203201

@@ -271,7 +269,13 @@ document.addEventListener('DOMContentLoaded', () => {
271269
function renderApiScorecard(sc, auditId) {
272270
document.getElementById('metric-score').innerText = `${sc.scamScore}%`;
273271
document.getElementById('metric-risk').innerText = `${sc.riskLevel} RISK`;
274-
document.getElementById('metric-monetization').innerText = sc.fcaStatus;
272+
document.getElementById('metric-monetization').innerText = sc.fcaStatus === 'WARNING_UNAUTHORISED_FIRM'
273+
? 'Warning-list pattern match'
274+
: 'Use official FCA checks';
275+
276+
const riskBadge = document.getElementById('risk-badge');
277+
riskBadge.className = `risk-badge ${sc.riskLevel === 'RED' ? 'risk-high' : sc.riskLevel === 'AMBER' ? 'risk-medium' : 'risk-low'}`;
278+
riskBadge.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> ${sc.riskLevel === 'GREEN' ? 'NO COMMON HIGH-RISK PHRASE DETECTED' : `${sc.riskLevel} CLAIM-RISK INDICATORS`}`;
275279

276280
const flagsContainer = document.getElementById('flags-container');
277281
flagsContainer.innerHTML = '';
@@ -289,7 +293,7 @@ document.addEventListener('DOMContentLoaded', () => {
289293
}
290294

291295
document.getElementById('reality-box-text').innerText = sc.mathReality;
292-
document.getElementById('verdict-text').innerText = `[API AUDIT LOG ${auditId}] FCA Status: ${sc.fcaStatus}. Evaluated by FinPulse AI REST Server.`;
296+
document.getElementById('verdict-text').innerText = `Pattern check ${auditId}: ${sc.fcaStatus}. This is not a scam determination or financial advice.`;
293297
}
294298

295299
function runBSScan(data) {
@@ -309,6 +313,118 @@ document.addEventListener('DOMContentLoaded', () => {
309313
document.getElementById('verdict-text').innerText = data.verdict;
310314
}
311315

316+
// --- Guided claim-to-Decision-Receipt demo ---
317+
const rehearseBtn = document.getElementById('rehearse-btn');
318+
const receiptBtn = document.getElementById('receipt-btn');
319+
const rehearsalResults = document.getElementById('rehearsal-results');
320+
const decisionReceipt = document.getElementById('decision-receipt');
321+
const decisionError = document.getElementById('decision-error');
322+
let activeRehearsal = null;
323+
let selectedComprehensionOption = null;
324+
325+
if (rehearseBtn) {
326+
rehearseBtn.addEventListener('click', async () => {
327+
const amount = Number(document.getElementById('decision-amount').value);
328+
const reason = document.getElementById('decision-reason').value.trim();
329+
decisionError.classList.add('hidden');
330+
rehearseBtn.disabled = true;
331+
rehearseBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Rehearsing downside...';
332+
333+
try {
334+
const response = await fetch(`${API_BASE_URL}/rehearsal`, {
335+
method: 'POST',
336+
headers: { 'Content-Type': 'application/json' },
337+
body: JSON.stringify({ claimText: claimInput.value, amount, reason })
338+
});
339+
const data = await response.json();
340+
if (!response.ok) throw new Error(data.error || 'Unable to run the rehearsal.');
341+
342+
activeRehearsal = { ...data, amount };
343+
selectedComprehensionOption = null;
344+
renderRehearsal(data);
345+
} catch (error) {
346+
decisionError.textContent = error.message;
347+
decisionError.classList.remove('hidden');
348+
} finally {
349+
rehearseBtn.disabled = false;
350+
rehearseBtn.innerHTML = '<i class="fa-solid fa-shield-heart"></i> Rehearse Maya\'s Decision';
351+
}
352+
});
353+
}
354+
355+
function renderRehearsal(data) {
356+
const missingList = document.getElementById('missing-context-list');
357+
missingList.innerHTML = '';
358+
data.missingContext.forEach(item => {
359+
const li = document.createElement('li');
360+
li.textContent = item;
361+
missingList.appendChild(li);
362+
});
363+
document.getElementById('downside-scenario').textContent = data.downsideScenario;
364+
document.getElementById('comprehension-question').textContent = data.comprehension.question;
365+
366+
const options = document.getElementById('comprehension-options');
367+
options.innerHTML = '';
368+
data.comprehension.options.forEach((option, index) => {
369+
const button = document.createElement('button');
370+
button.type = 'button';
371+
button.className = 'receipt-option';
372+
button.textContent = option;
373+
button.addEventListener('click', () => {
374+
options.querySelectorAll('.receipt-option').forEach(item => item.classList.remove('selected'));
375+
button.classList.add('selected');
376+
selectedComprehensionOption = index;
377+
receiptBtn.disabled = false;
378+
});
379+
options.appendChild(button);
380+
});
381+
382+
receiptBtn.disabled = true;
383+
rehearsalResults.classList.remove('hidden');
384+
decisionReceipt.classList.add('hidden');
385+
document.querySelectorAll('.flow-step').forEach((step, index) => step.classList.toggle('active', index < 3));
386+
rehearsalResults.scrollIntoView({ behavior: 'smooth', block: 'start' });
387+
}
388+
389+
if (receiptBtn) {
390+
receiptBtn.addEventListener('click', async () => {
391+
if (!activeRehearsal || selectedComprehensionOption === null) return;
392+
receiptBtn.disabled = true;
393+
try {
394+
const response = await fetch(`${API_BASE_URL}/decision-receipt`, {
395+
method: 'POST',
396+
headers: { 'Content-Type': 'application/json' },
397+
body: JSON.stringify({
398+
rehearsalId: activeRehearsal.rehearsalId,
399+
amount: activeRehearsal.amount,
400+
selectedOption: selectedComprehensionOption
401+
})
402+
});
403+
const data = await response.json();
404+
if (!response.ok) throw new Error(data.error || 'Unable to create the receipt.');
405+
renderDecisionReceipt(data);
406+
} catch (error) {
407+
decisionError.textContent = error.message;
408+
decisionError.classList.remove('hidden');
409+
receiptBtn.disabled = false;
410+
}
411+
});
412+
}
413+
414+
function renderDecisionReceipt(data) {
415+
document.getElementById('receipt-id').textContent = data.receiptId;
416+
document.getElementById('receipt-created').textContent = new Date(data.createdAt).toLocaleString('en-GB');
417+
document.getElementById('receipt-amount').textContent = ${activeRehearsal.amount.toLocaleString('en-GB')}`;
418+
document.getElementById('receipt-next-step').textContent = data.nextStep;
419+
document.getElementById('receipt-privacy').textContent = data.privacy;
420+
const status = document.getElementById('receipt-status');
421+
status.textContent = data.learningStatus === 'CORE_RISK_UNDERSTOOD' ? 'Core risk understood' : 'Review recommended';
422+
status.className = `receipt-status ${data.learningStatus === 'CORE_RISK_UNDERSTOOD' ? 'understood' : 'review'}`;
423+
decisionReceipt.classList.remove('hidden');
424+
document.querySelectorAll('.flow-step').forEach(step => step.classList.add('active'));
425+
decisionReceipt.scrollIntoView({ behavior: 'smooth', block: 'center' });
426+
}
427+
312428
// --- 3. UK Payslip Decoder Engine (HMRC API Connected) ---
313429
const salarySlider = document.getElementById('salary-slider');
314430
const salaryDisplay = document.getElementById('salary-display');
@@ -360,13 +476,17 @@ document.addEventListener('DOMContentLoaded', () => {
360476
}
361477

362478
function renderPayslipResults(s) {
363-
document.getElementById('pay-gross').innerText = ${s.grossSalaryMonthly.toLocaleString()}`;
364-
document.getElementById('pay-tax').innerText = `-£${s.payeTaxMonthly.toLocaleString()}`;
365-
document.getElementById('pay-ni').innerText = `-£${s.nationalInsuranceMonthly.toLocaleString()}`;
366-
document.getElementById('pay-student').innerText = `-£${s.studentLoanMonthly.toLocaleString()}`;
367-
document.getElementById('pay-pension').innerText = `-£${s.employeePensionMonthly.toLocaleString()}`;
368-
document.getElementById('pay-match').innerText = `+£${s.freeEmployerPensionMatchMonthly.toLocaleString()}`;
369-
document.getElementById('pay-net').innerText = ${s.netTakeHomeMonthly.toLocaleString()}`;
479+
document.getElementById('ps-gross').innerText = ${s.grossSalaryMonthly.toLocaleString()}`;
480+
document.getElementById('ps-tax').innerText = `-£${s.payeTaxMonthly.toLocaleString()}`;
481+
document.getElementById('ps-ni').innerText = `-£${s.nationalInsuranceMonthly.toLocaleString()}`;
482+
document.getElementById('ps-student-loan').innerText = `-£${s.studentLoanMonthly.toLocaleString()}`;
483+
document.getElementById('ps-pension').innerText = `-£${s.employeePensionMonthly.toLocaleString()}`;
484+
document.getElementById('ps-net-pay').innerText = ${s.netTakeHomeMonthly.toLocaleString()}`;
485+
document.getElementById('ps-total-deductions').innerText = ${(s.payeTaxMonthly + s.nationalInsuranceMonthly + s.studentLoanMonthly + s.employeePensionMonthly).toLocaleString()}`;
486+
document.getElementById('ps-pension-label').innerText = `Employee Pension (${appState.pensionPct}%)`;
487+
document.getElementById('ps-tax-code').innerText = taxCodeSelect ? taxCodeSelect.value : '1257L';
488+
const payslipTip = document.getElementById('payslip-tip');
489+
if (payslipTip) payslipTip.textContent = `Illustrative employer contribution: £${s.freeEmployerPensionMatchMonthly.toLocaleString()}/month. Check your scheme rules before deciding.`;
370490

371491
if (document.getElementById('annual-net-summary')) {
372492
document.getElementById('annual-net-summary').innerText = `Annual Net Take-Home Pay: £${s.netTakeHomeAnnual.toLocaleString()}`;

0 commit comments

Comments
 (0)