Sentinel
June 2026
Overview
Transaction monitoring is good at finding suspicious movement. It is less useful when the customer itself changes slowly enough that no single transaction looks wrong.
That is KYC drift: the gap between the business a bank originally approved and the business it has become. A sustainable-footwear company might move toward AI infrastructure or digital assets one filing, appointment, and partnership at a time. Each signal can look harmless in isolation while the original risk profile quietly expires.
AMINA Bank made that problem its SwissHacks 2026 challenge. In 40 hours, our team of five built Sentinel, a dynamic risk-profiling system that compares public intelligence with a customer’s onboarding baseline. The design had three constraints from the start: expensive reasoning should be rare, every alert should be explainable, and a human compliance officer should remain in control.
I scaffolded the TypeScript monorepo and owned the public-data connectors, ingestion hardening, ownership graph, LLM integration, and the post-hackathon deployment. A teammate led the drift and confidence engines, governance state machine, core schemas, and most of the interface. We shared the middle of the escalation pipeline.
The system
Sentinel has two layers of intelligence. The first collects external signals such as news, SEC filings, enforcement notices, ownership changes, and sanctions exposure. The second is a simulated internal KYC baseline describing what the bank expects each customer to do, who controls it, and how risky it was at onboarding.
Everything meets in a framework-agnostic core package built from pure functions and Zod schemas. Connectors accept an injected fetch implementation and do not reach into environment variables, so the same pipeline runs from offline extraction scripts and live SvelteKit routes. That boundary became important later when the public demo moved to Cloudflare Workers.
Cheap first, deep only when it matters
The core pipeline is a cost-aware cascade. Stage 0 uses multilingual keyword rules to route each signal into five drift axes: business model, ownership, scale, jurisdiction, and reputation. Stage 1 aggregates those signals with source confidence, recency decay, and a corroboration bonus. Those two stages are deterministic and effectively free.
Only axes that actually moved reach Stage 2, where Apertus-8B decides whether the movement is materially relevant to the customer’s baseline. Apertus-70B performs the final synthesis only when the composite risk crosses the alert threshold or jumps sharply since the previous evaluation. Its output must include cited signals, the triggering axes, and a recommended action.
This was not just an optimization. A Swiss-hosted, open-weight model gave us a credible auditability and data-residency story for a Swiss bank, while the cascade made the expensive model the exception rather than the default. The dashboard exposes the same decision through a cost funnel, so the reader can see which entities stopped at deterministic scoring and which reached deep reasoning.
From public noise to one risk story
The live data spine combined EventRegistry news with SEC EDGAR filings, SEC enforcement feeds, and multilingual RSS. EventRegistry limited both result count and historical range, so its connector split requests into concurrent time windows with retry and backoff. EDGAR provided the durable history: deterministic 8-K item routing turned structural filings into high-confidence signals even when the news API could not look far enough back.
All sources normalize into one Signal schema. A bounded-concurrency pool protects upstream services, watermark state fetches only unseen events, and union-find clustering collapses the same event reported by several sources. The highest-confidence signal survives while the cluster size records corroboration.
Some integrations were scaffolded rather than live. Without credentials they return nothing and the demo falls back to clearly bundled fixtures. That rule kept us from presenting synthetic data as a live feed simply because it made the screen look fuller.
Risk travels through relationships
A company’s own signals are only part of its exposure. Sentinel builds a typed graph of entities, people, countries, and wallets connected by ownership, control, investment, board, operating, and transaction relationships.
The risk walk treats those edges as undirected. A sanctioned controller can taint the company it controls, but a risky company should also bring its controllers into view. Breadth-first search produces the shortest explainable path, and confidence decays at each hop so a direct relationship carries more weight than a distant one.
The graph serves two purposes. It enriches an alert with hidden exposure, such as a two-hop route to a sanctioned owner, and it re-evaluates connected customers when confirmed drift should propagate through the book. The result is not just a risk score but a relationship chain an analyst can inspect.
An alert has to survive review
Every model response is schema-validated, and every final alert must resolve at least one citation. If the model invents an identifier, the pipeline falls back to the highest-confidence source signals rather than publishing an ungrounded explanation.
Every ingestion, score, escalation, human action, and outcome is then written to an append-only hash-chained audit log with token and cost accounting. A maker-checker state machine keeps the final decision human: an analyst can escalate a case, but a separate compliance officer must approve or dismiss it.
The interface tells the same story visually. A timeline replays the case while the drift radar, ownership graph, cost funnel, and audit events update together. I built the rotating client-book globe, timeline playback, searchable risk-sorted book, and audit toasts; my teammate built most of the analytical views around them.
Publishing without publishing the keys
After SwissHacks I deployed Sentinel publicly on Cloudflare Workers. Running the live LLM cascade for every visitor would have been expensive and would have placed provider keys and API code too close to the public surface, so the deployed site uses fixture replay.
An offline capture command runs the same shared escalation pipeline over the two hero customers and stores the complete result. The web route replays those captured stages through the real audit log and cost model, so the interface behaves the same without making an external model call. Customers without captured analysis simply do not expose the Analyze action.
Cloudflare has no filesystem, so the customer book, signals, pattern library, and captured analyses are inlined at build time with import.meta.glob and parsed through the existing Zod boundary. I also separated the cost model from the provider client so the public bundle can price every tier without importing any live-API surface.
The three-minute pitch
Sentinel did not reach the AMINA track final. Two teams advanced from roughly eleven, selected on a three-minute presentation before the longer Q&A. The judges told us afterward that the presentation had not surfaced the depth of the engineering.
The Q&A went differently. I walked through how the Allbirds case moved from public signals to a drift prediction and showed the working dashboard; the judges engaged with the backend and were satisfied with the answers. That contrast was useful. We had treated the pitch as something separate from the system, but for a judged product, the explanation is part of what has to work.
The public deployment was my second attempt at that explanation. It keeps the system available after the weekend and lets the cascade, graph, governance, and cost story speak through the product itself.
