A scammer doesn't break into the bank. They talk your mother into wiring the money herself. HyperGuard is the AI swarm that picks up the phone in the sixty seconds before she sends it.
Detection tells you after. HyperGuard intervenes during, and helps recover after.
Every fraud system a bank owns is built to answer one question: is this transaction legitimate? For modern scams, the answer is always yes. The password was right. The device was recognised. The victim tapped Send with her own thumb, because a man claiming to be the police spent forty minutes convincing her she had to.
HyperGuard asks a different question: is this person being manipulated right now?
The moment a transfer looks nothing like the customer's normal behaviour, five agents move at once. One calls her. One listens to her answers and recognises which scam script she's being read from. One wakes up her son. And the money stays in the account until a decision with a written rationale says otherwise.
Put a defender in the room at the exact moment someone is about to be robbed.
The attack surface stopped being the system and became the person, and the industry's tooling hasn't caught up.
- Roughly 94% of fraud losses are payments the victim authorised themselves. Nothing was stolen — it was given. Every control that checks credentials, devices, or account takeover signals waves these straight through.
- The whole scam lives inside a sixty-second window — between "I'll send it" and "sent." No human fraud team can be inside that window for every customer, on every transfer, at 2am.
- The existing shields report; they don't intervene. Tools like Singapore's ScamShield are excellent at detection and reporting, which happen after the money has moved. Nobody is doing real-time voice intervention, live scam classification, family escalation, and recovery orchestration as one loop.
Blocking the transfer outright isn't the answer either — it fails the customer and never explains why. The victim needs to understand they're being scammed while it's still reversible. That takes a conversation, and a conversation at that scale takes agents.
Seeded scenarios run through the full swarm on a fresh clone with zero API keys configured — every external dependency degrades to a deterministic in-process simulation, so the orchestration is provable before anything is wired up. Two of the four, verbatim:
$ python seed.py
HyperGuard smoke test, capabilities: {'llm': False, 'telephony': False, 'speech': False,
'persistence': False, 'distributed_bus': False,
'demo_mode': True}
──────────────────────────────────────────────────────────────────────────────
scenario police_impersonation
customer May Tan · SGD 8,000 → Quik Holdings Pte Ltd
risk 99% (critical)
scam Government / Police Impersonation
decision BLOCK
guardians 1 alerted
evidence built
narrative Blocked. The SGD 8,000 transfer to Quik Holdings Pte Ltd was halted
after the call surfaced Government / Police Impersonation; Marcus Tan
was alerted. The money never left the account, and a recovery evidence
package was prepared for reporting the beneficiary.
──────────────────────────────────────────────────────────────────────────────
scenario legitimate_transfer
customer May Tan · SGD 280 → NTUC FairPrice
risk 7% (minimal)
scam —
decision APPROVE
guardians 0 alerted
evidence n/a
narrative Approved. The SGD 280 transfer to NTUC FairPrice matched May's
established behaviour with no scam indicators, released without
interruption.
──────────────────────────────────────────────────────────────────────────────The grocery run clears in milliseconds and nobody is ever called. That restraint is the product: an intervention layer that fires on everything is a layer banks switch off.
Five agents on a LangGraph state machine. Nodes are agents; edges are conditional transitions on risk_score, verification_status and scam_detected. A single shared state object carries the transaction, the customer's behavioural profile, the risk assessment, the live transcript, the scam classification, the verdict and the evidence through the graph.
Every customer carries a behavioural baseline: typical amounts, known payees, active hours, transfer velocity. Each incoming transfer is scored against her history, not a population average, and the signals combine through a logistic link into a 0–1 risk score.
The weights are legible constants, not a black box. A first-ever payee contributes the most evidence (it is the single strongest predictor of authorised-push fraud); an amount several σ above her normal, off-hours activity, a velocity burst, an overseas number, and coercion vocabulary in the transfer note all stack on top. Below 0.58 the transfer clears silently. At 0.88 it is held pending human confirmation regardless of what the call finds.
This is deliberately not a deep model. At the moment of intervention the explanation matters as much as the number — the customer on the phone needs to hear why, and an investigator needs to read it six months later.
The instant risk crosses the threshold, an outbound call is placed via Twilio with an ElevenLabs voice. It isn't an IVR reading a warning; it conducts contextual verification — who asked you to send this, what did they say would happen if you didn't — and streams the transcript back into the graph turn by turn. When telephony credentials are live the conversation runs out-of-band over voice webhooks; when they aren't, the same dialogue plays out deterministically in-process.
Scams are not improvised. They run from a small number of scripts, and each one has a tell. The Educator matches the live transcript against six archetypes — government/police impersonation, bank impersonation, investment/crypto, romance, job/task, and tech support — and feeds the counter-line straight back into the call for the Negotiator to say aloud. For police impersonation, that's the sentence no real agency ever crosses: we will never ask you to move money to a safe account.
Naming the script is what breaks it. A generic "this may be a scam" warning is easy for a victim mid-manipulation to dismiss. Being told exactly what the person on the other line is about to say next is not.
How it actually works. This is an indicator-density classifier, not an LLM — each archetype carries a list of linguistic fingerprints, and confidence scales with how many distinct ones light up. Same reasoning as the Digital Twin: the counter-lines are read aloud to a frightened person and quoted back in an audit trail, so they are authored and deterministic rather than generated. The LLM's judgement enters after the call, in the adjudicator (services/reasoning.py), where it weighs the customer's actual answers and can disagree with the pattern match. Swapping in LLM classification with this matcher as a strictness floor is on the roadmap — see FUNCTIONALITY.md.
Some victims cannot be talked down by a stranger, especially one calling from the bank when a "policeman" has just told them the bank is compromised. The Guardian escalates to pre-authorised trusted contacts with the transaction context and the risk rationale, adding a human verification layer exactly where it's needed most.
When fraud has already been processed, the swarm runs a different path entirely and assembles an evidence package: transaction trail, the risk signals that fired, the full conversation log, and the beneficiary details, formatted for the bank's recovery team and law enforcement. Speed is everything in fund recovery, and this turns a multi-day paperwork exercise into one click.
And the Arbiter decides. It weighs the risk score, the verification status and the scam classification into a single verdict with a written rationale attached. Every decision the swarm makes is explainable and audited — which is the only way a layer like this ever passes a bank's compliance review.
frontend/ |
The bank's mission control. Live risk meters, streaming transcripts, the agent relay firing in real time, every case filed with its full decision trail. Next.js, with a bespoke "interdiction console" design system, a React-Three-Fiber hero and GSAP scroll choreography. | |
mobile/ |
The customer's side. A working Expo banking app — balances, payees, transfers, next-of-kin — so the swarm has real transactions to act on. Send money to the hidden scam payee and watch the intervention land on your own phone. | |
backend/ |
FastAPI + LangGraph. REST, WebSocket event stream, wallet and admin APIs, and the five agents. Live on Railway → |
git clone https://github.com/basil-boh/HyperGuard-.git && cd HyperGuard-
cp infra/.env.example .env # optional — it runs without any of itBackend — the swarm, and a one-command proof it's wired correctly:
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python seed.py # run every scenario through the full swarm
pytest # risk engine · swarm · wallet
uvicorn app.main:app --reload --port 8000Control centre — cd frontend && npm install && npm run dev → localhost:3000
Wallet — bind the backend to your LAN first (uvicorn app.main:app --host 0.0.0.0 --port 8000), then cd mobile && npm install && npx expo start and open it in Expo Go. The app auto-discovers the backend from the Expo dev host, so there is nothing to configure on the phone. See mobile/README.md for the demo script.
The wallet opens on a sign-in screen: phone number + 6-digit PIN. The database ships with seven customers, each with a different financial signature — because the risk engine scores against learned behaviour, the only honest way to test it is with accounts that genuinely differ. Tap Demo accounts on the sign-in screen to pick one without typing.
| Phone | PIN | Customer | Signature | Good for testing |
|---|---|---|---|---|
+6580001234 |
112233 |
Alex Tan, 67 | Retiree, small regular outgoings | The default demo — two planted scam payees |
+6580000001 |
445566 |
May Tan, 72 | Retiree, two scams already on file | Repeat-target escalation, populated case history |
+6580000002 |
778899 |
Daniel Lim, 34 | Salaried professional, high volume | Four-figure transfers that are genuinely fine |
+6580000003 |
102030 |
Wong Ah Kow, 81 | Thin file, tiny amounts | Any four-figure transfer reads as critical |
+6580000004 |
135791 |
Priya Nair, 58 | Steady bills and school fees | Tech-support interception with a follow-up call |
+6580000005 |
246810 |
Siti Rahman, 29 | Gig worker, many small payouts | High velocity, low amounts |
+6580000006 |
909090 |
Robert Chen, 46 | SME owner, five-figure supplier runs | Big ≠ suspicious |
+6580000010 |
321321 |
Marcus Tan, 41 | Guardian — son to Alex and May | The guardian inbox, with incidents already on file |
+6580000013 |
654654 |
Linda Wong, 52 | Guardian — daughter to Wong | A second guardian, one unread incident |
The contrast is the point: the same SGD 8,000 transfer is a 99% critical block for Wong and unremarkable for Robert. Each account carries its own balance, saved payees, guardians and 19–31 transactions of history, plus at least one hidden scam payee to transfer to.
Credentials are served by GET /api/auth/demo-accounts and rendered in the app. Set EXPOSE_DEMO_CREDENTIALS=false to hide them, and ALLOW_HEADER_USER_OVERRIDE=false to require a real token (the legacy X-User-Id header is honoured by default so curl demos keep working).
PINs are stored as PBKDF2-HMAC-SHA256 hashes, never in plain text; sessions are stateless HMAC-signed bearer tokens. Repeated failures lock a phone number out for 60 seconds.
The hardest part of protecting someone from a scam is that the person best placed to help — their son, their daughter — finds out afterwards, if at all. The Network tab makes that relationship a first-class object rather than a phone number in a settings screen.
A guardian adds a relative by phone number, and the relative decides: the invitation sits pending until they accept, and nothing about their account is visible until they do. Accepting does three things at once — the guardian appears on the account's alert list so the swarm can reach them mid-intervention, every blocked case already on file is delivered to their inbox, and future ones arrive automatically. Either side can revoke at any time; the link is kept, marked revoked, so past access stays auditable.
The relationship also builds from the other direction: adding a trusted contact whose phone already belongs to a HyperGuard account links the two immediately, because the person whose consent matters is the one doing the adding.
flowchart LR
G["Guardian<br/>invites by phone"] -->|pending| P["Relative<br/>accepts or declines"]
P -->|accepted| L["Link active"]
L --> A["On the swarm's<br/>alert list"]
L --> H["History<br/>backfilled"]
B["Transfer blocked"] --> R["Incident report<br/>→ guardian's inbox"]
R --> F["Alert authorities<br/>(SIMULATED)"]
classDef base fill:#07080c,stroke:#2a2f26,stroke-width:1px,color:#d7dfc8
classDef good fill:#0c1408,stroke:#c9f24a,stroke-width:1.5px,color:#c9f24a
classDef warn fill:#171207,stroke:#ffc24b,stroke-width:1.5px,color:#ffc24b
class G,P,B base
class L,A,H,R good
class F warn
An incident report is the whole account of what happened — the risk signals that fired, the call transcript, the scam pattern, the guardian actions, the verdict — read live from the case, so it can never drift from the record.
On the authorities filing. The "Alert authorities" action is a simulation, and the code goes out of its way to keep it impossible to mistake for anything else: every reference is prefixed SIM-, every payload carries simulated: true plus a disclaimer naming the real channel (ScamShield, 1799), the status timeline stops at referred and never invents an outcome like funds recovered or an arrest, and services/filing.py has no network access of any kind. HyperGuard is not connected to the police, the National Anti-Scam Centre, or any other body. If you later wire up a real integration, that module is the seam — and the labelling should be the last thing removed, not the first.
A guardian can cap what the person they protect can send in one transfer. It's checked before the swarm runs, because it isn't a risk judgement — it's a standing instruction from someone this customer trusted while they were clear-headed, and the whole point is that it holds when they aren't.
Only the guardian can change it. That asymmetry is the feature: a limit the victim can raise mid-call is a limit a scammer can talk them through raising. The protected person always sees the limit and who set it, on their own balance card and next to that guardian's name — and if they disagree, they can revoke the guardian entirely. That's the escape hatch, and it's deliberately not something you can do in thirty seconds while someone is shouting at you on the phone.
Where several guardians each set one, the lowest wins, so adding a guardian can only ever tighten protection. Revoking a link drops its limit automatically. Nothing is seeded with a limit, so no existing demo path changes until you set one.
Most transfers never reach a model at all: the Digital Twin's risk score is a deterministic logistic function, so the common path is free, not merely cheap. Of the cases that do reach one, most are ordinary verification calls a small model handles well. The expensive model is reserved for cases that have actually escalated.
| Call | Tier | Why |
|---|---|---|
| Negotiator's opening line | fast | The customer is on the phone. Time-to-first-word is the product. |
| Negotiator's follow-up turns | fast → deep on escalation | Exploratory questions are cheap work; once a pattern is confirmed the stakes change. |
| Educator's guidance line | no model | The safety wording is authored, not generated. |
| Post-call assessment | deep | It decides whether a family member gets woken up. Nobody is waiting. |
| Incident report | deep | It's the artefact a human reads. |
The promotion rule is one readable predicate in services/model_policy.py: a confirmed scam pattern, or risk at/above the hard-block threshold. Everything else stays on the fast model.
LLM_MODEL_FAST=gpt-5.5-mini # routine work
LLM_MODEL_DEEP=gpt-5.5 # escalated cases + written outputs (falls back to LLM_MODEL)Every call is recorded — tier, model, latency, tokens — and the per-case ledger rides along on the intervention poll as model_usage, so the saving is measurable rather than asserted. Set the optional LLM_PRICE_* variables and it also reports dollars, including what the same case would have cost on the deep model throughout. Left unset it reports tokens and latency only: a guessed price is worse than no price.
Seeded so the network is populated on first load: Marcus watches Alex and May with two of May's incidents in his inbox (one unread, one already filed), Linda watches her father, and a pending invitation from Marcus to Wong Ah Kow is waiting so the accept flow can be demonstrated on a single device.
Every external dependency is capability-gated. Absent a credential, that subsystem degrades to a deterministic simulation and the swarm still runs end to end — you can see the whole thing work before signing up for anything.
| Key | What it buys you | Needed? |
|---|---|---|
OPENAI_API_KEY |
LLM dialogue and the risk explainer | Recommended. Without it, scripted dialogue and template explanations. |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_PHONE_NUMBER |
places a real outbound call | Optional. Without it, the conversation runs in-process. |
ELEVENLABS_API_KEY |
the negotiator's voice | Optional. Falls back to Twilio's Polly TTS. |
SUPABASE_URL / SUPABASE_SERVICE_KEY |
cases and transcripts survive a restart | Optional. Without it, in-memory. |
REDIS_URL |
mirrors every swarm event to a Redis stream, for audit and replay | Optional. The bus itself is in-process either way — nothing consumes the stream yet, so this does not make the app multi-worker. |
AUTH_SECRET |
signs customer session tokens | Local: no. Deployed: yes, or every restart signs everyone out. |
LLM_MODEL_FAST / LLM_MODEL_DEEP |
cheap model for routine work, powerful one for escalated cases | Optional. Both fall back to LLM_MODEL. |
LLM_PRICE_*_INPUT / _OUTPUT |
turns recorded token counts into a cost estimate | Optional. Unset, the API reports tokens and latency but never dollars. |
Project layout and the developer contract
backend/ # FastAPI + LangGraph
app/
graph.py # the swarm orchestrator — nodes, routers, shared state
config.py # capability gating: every integration optional
agents/ # digital_twin · negotiator · educator · guardian · recovery · arbiter
services/ # risk_engine · scam_taxonomy · dialogue · baselines · llm · auth
integrations/ # voice (Twilio/ElevenLabs) · notifications · event bus · persistence
api/ # routes · ws · auth · wallet (customer) · admin (console) · twilio_voice
wallet/ # multi-account bank: accounts, cases, payee registry
data/seed_data.py # demo personas & scenarios
db/schema.sql # Postgres schema
tests/ # risk engine · end-to-end swarm · wallet · auth
seed.py # one-command smoke test
frontend/ # Next.js — the bank's CONTROL CENTRE
app/console/ # overview · users/[id] · cases/[id] · live
components/ # landing (Hero3D · ScrollStory) · control · console (RiskMeter · TranscriptStream)
mobile/ # Expo — the CUSTOMER's banking app
app/ # Wallet · Activity · Guardians · transfer · intervention
components/ # AgentRelay · RiskGauge · Transcript
infra/ # .env.example · Supabase schema
The entire swarm is one call. Everything else — the REST API, the WebSocket stream, the wallet — is a caller.
from app.graph import get_orchestrator
outcome = await get_orchestrator().run(customer, txn)
outcome.decision # Decision.approve | Decision.block
outcome.risk # score, band, and every signal that fired, with reasons
outcome.classification # which scam archetype, and how confident
outcome.transcript # the conversation, turn by turn
outcome.guardian_alerts # who was contacted, and what they were told
outcome.evidence # the recovery package, when one was needed
outcome.narrative # the whole thing in plain EnglishToday HyperGuard is a complete, demonstrable swarm running against a simulated bank. The path to production is about depth, not new ideas.
- Live rails. The telephony, voice and LLM paths are capability-gated and already wired — flipping the keys on places a real call. Next is hardening that path against real conversation: interruptions, dialect, hostility, and the scammer still being on the other line.
- Durable recovery. Recovery is the one flow that genuinely outlives a process — evidence gathering, bank submission, police reporting, status chasing. That belongs in Temporal, not in memory.
- A twin that learns. Today's baselines are computed from transaction history with legible weights. The next version updates continuously and learns what an intervention taught us about that customer — including the ones we called and shouldn't have.
- The layer, not the app. The end state isn't a banking app; it's a drop-in layer between intent and settlement that any bank or wallet can put in front of a transfer, with no rip-and-replace of their stack.
plan.md has the full build plan — data model, phase timeline, and the orchestration decisions with their trade-offs written down. PRESENTATION.md is the live demo script; PITCH.md is the forty-second version.
HyperGuard is a prototype built for demonstration. It is not a production financial-security system and is not connected to real banking or payment rails. Do not use it to make real fraud-prevention decisions without appropriate review, testing, and compliance sign-off. The seeded personas, transactions and scam payees are fictional.