An open-source assessment framework and reference implementation for fraud prevention and operational resilience in irrevocable instant-payment systems.
Live site · Methodology · Documentation source
Pre-1.0. This is an active reference implementation, not a released product. Being precise about the line, because the difference matters if you are evaluating it:
| Area | State |
|---|---|
| Decision engine, Layers 1–3 (in-path) | Implemented, runs locally |
| Layers 4–5 (async enrichment, post-settlement) | Implemented, event-driven |
| Immutable audit trail | Implemented |
| Assessment engine (12 categories, maturity 0–4) | Implemented |
| Methodology documents | Complete — 11 documents in docs/framework |
| Landing site + methodology rendering | Live |
| Dashboard and transaction explorer | Not built |
Benchmarks (backend/benchmarks) |
Module exists, suite has not been run |
| Public demo API | Not deployed |
| v1.0.0 tag | After benchmarks and the quality sweep |
Every figure in this repository, on the site, and in the seeded local run is SYNTHETIC / DEMO DATA. No number here is a benchmark result, a production measurement, or a claim about any institution. Where the framework states a latency figure it is a design budget, labelled as such.
Two things that depend on each other:
- A methodology — how to assess fraud controls and operational resilience on an instant-payment rail. Eleven documents in
docs/framework, grounded in the public record and cited to primary sources. This is the primary artifact. - A reference implementation — a working, deterministic decision engine that demonstrates the methodology, so the claims are checkable rather than asserted. Java 21, Spring Boot 3, modular monolith.
It is built to be independently useful. Clone it, read the methodology, run a sample assessment and a local evaluation, with no commercial service involved.
On a card or ACH rail, a suspicious payment can be investigated after the fact. Chargeback windows, reversal mechanisms and settlement delays give an institution days. Control design can assume an investigation window exists.
An instant payment settles in seconds and cannot be recalled. That removes the window, and three consequences follow:
- The decision is the whole control. There is no second look. Whatever the system concludes in the authorization path is final.
- Latency is a correctness property, not a performance goal. A control that is right but slow is a control that fails the payment.
- False positives are failed payments. Declining a legitimate transaction is not a conservative choice — it is an outage for that customer, at the moment they needed the rail to work.
Most fraud programs are optimised against losses prevented, a number that is easy to measure and easy to attribute. The cost on the other side of the ledger is diffuse and mostly unmeasured. IPRF treats both as first-class. See false-positive-model.md.
Decide before the transaction arrives what can be evaluated in-path, and what must be pre-computed or evaluated asynchronously.
Everything else is downstream of that sentence. The failure it prevents is specific: a control that is correct in isolation but, placed on the authorization path, performs a query. Under normal load nobody notices. Under the load where it matters, the institution starts failing legitimate payments at exactly the moment it most needs to be working.
Concretely:
- In-path (Layers 1–3) — deterministic, bounded latency, pre-computed state only. No live database query during authorization. Layer 3 reads pre-computed risk state from Redis, never a synchronous lookup against the primary database.
- Asynchronous (Layers 4–5) — enrichment, external intelligence, heavy analytics, post-settlement analysis. Feeds future decisions. Never blocks the payment path.
The boundary is enforced, not just documented: an ArchUnit guard fails the build if the HTTP boundary starts making risk judgements, and a test asserts that a hanging enrichment registry cannot affect caller latency.
| Layer | Name | Path | Budget | What it evaluates |
|---|---|---|---|---|
| 1 | Identity & Account Posture | in-path | < 1 ms | Account age, verification, device, channel, history |
| 2 | Real-Time Behavioral Scoring | in-path | < 5 ms | Amount, counterparty, timing, channel, velocity |
| 3 | Counterparty & Network Signals | in-path | < 5 ms | Pre-computed counterparty risk state, network relationships |
| 4 | External Enrichment | async | background | External intelligence, sanctions and watchlists, context |
| 5 | Post-Settlement Analysis | async | continuous | Typology detection, pattern discovery, feedback to future decisions |
Budgets are design targets. Full definitions in fraud-control-layers.md; the latency reasoning is in latency-model.md.
flowchart LR
C([Client]) -->|POST /evaluate| API[transaction-api]
subgraph SYNC["IN-PATH — deterministic, bounded, no live queries"]
direction TB
API --> L1[Layer 1<br/>Identity & posture]
L1 --> L2[Layer 2<br/>Behavioral scoring]
L2 --> L3[Layer 3<br/>Counterparty & network]
L3 --> D{{ALLOW / REVIEW / DECLINE}}
end
L3 -.reads.-> RS[(risk-state<br/>Redis — pre-computed)]
D --> AUD[(audit<br/>append-only)]
D --> C
D -->|event, non-blocking| MQ[[RabbitMQ]]
subgraph ASYNC["ASYNCHRONOUS — feeds future decisions"]
direction TB
MQ --> L4[Layer 4<br/>External enrichment]
MQ --> L5[Layer 5<br/>Post-settlement analysis]
L4 --> RS
L5 --> RS
end
The event is published after the decision is made; the publisher hands off to an executor and returns, so nothing downstream can affect the caller's latency.
Detail in architecture.md.
curl -s http://localhost:8080/api/v1/transactions/evaluate \
-H 'Content-Type: application/json' \
-d '{
"transactionId": "txn_demo_001",
"payerAccountId": "acct_123",
"payeeAccountId": "acct_987",
"amount": 125.00,
"currency": "USD",
"channel": "MOBILE_APP",
"rail": "FEDNOW",
"deviceId": "dev_a1b2c3",
"initiatedAt": "2026-09-09T14:31:07Z"
}'Nothing is omitted for brevity — a response reporting only the decision and the score would be a black box with extra steps. The contributing factors, the per-layer detail and the rule versions are the point.
Deterministic: the same input under the same rule versions always produces the same output. Thresholds live in configuration (application-rules.yml), not in code. There is no model, no black-box score, and no AI in the decision path — machine learning is an extension point, not a dependency.
An institution scores itself across twelve categories, each at a maturity level from 0 to 4. The assessment model defines the categories, the structure of a control, what counts as evidence, and how findings and recommendations are produced. Scoring rules live in configuration so the model can be adjusted without changing code.
assessment-model.md— categories, controls, evidencematurity-model.md— the five levels and how scores compute
A legitimate payment that is declined is a failed payment. The framework tracks, and requires an institution to track, the detection rate and the false-positive rate, alongside decline / review / approval rates and p50 / p95 / p99 latency. Reporting one without the others describes half the system.
REVIEW exists because instant rails explicitly accommodate a fraud-suspicion hold — it is the mechanism that lets an institution be careful without failing the payment outright.
Fraud controls that are unavailable are not fraud controls. The resilience model assesses failure isolation, dependency coupling, recovery time, and whether remediation after an incident is permanent or ceremonial.
Its central thesis has its own document: growth coupling — when recovery time is a function of business volume. It is one of the few failure modes that gets steadily worse while every individual deployment looks fine, and it is invisible to the metrics most institutions watch.
For every typology specific to irrevocable instant payments, the threat model states which layer is supposed to catch it and what happens when that layer is absent. That makes coverage checkable rather than assumed. See threat-model.md.
Requirements: JDK 21, Node 22, Docker.
git clone https://github.com/ronaldobinho/iprf-framework.git
cd iprf-framework
cp .env.example .env # local values, synthetic data only
docker compose up -d # PostgreSQL 16, Redis 7, RabbitMQ
cd backend && ./gradlew build # compile + tests
./gradlew :transaction-api:bootRun # http://localhost:8080A startup seeder loads synthetic payer profiles so a locally running API returns differentiated decisions instead of REVIEW for every unknown payer. It is demo data and is disabled outside local profiles.
The frontend:
cd frontend
npm ci
npm run dev # http://localhost:3000docker-compose.yml provisions the three dependencies on an isolated network: PostgreSQL 16, Redis 7, RabbitMQ. Configuration comes from .env; see .env.example. Secrets are read from environment variables only — none are committed.
cd backend && ./gradlew testCoverage is deliberate about the cases that break fraud pipelines in production rather than in demos: duplicate events, timeouts, dependency failure, stale risk state, and false-positive scenarios. Event handlers are idempotent and tested as such. An ArchUnit rule guards the in-path contract.
The frontend currently has no test suite — npm test runs with --passWithNoTests.
The benchmarks module is where JMH microbenchmarks and a load harness will live, with a reproducible ./gradlew benchmark entry point writing results plus an environment fingerprint.
It has not been run. No latency number anywhere in this repository is a measurement. When the suite runs, results will be published with the exact command and environment that produced them, and never presented as numbers a reader cannot reproduce.
The landing site is a static export with no backend dependency: it explains the framework, walks a transaction through all five layers, and renders the methodology documents directly from docs/framework — the same files, not a summary rewritten for a website.
A publicly hosted demo API is on the roadmap. It does not exist yet; the API examples above are for a local run.
One endpoint today:
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/transactions/evaluate |
Evaluate a transaction, return an explainable decision |
With the API running locally:
- OpenAPI document —
http://localhost:8080/api/v1/openapi - Swagger UI —
http://localhost:8080/api/v1/docs
Every decision is written to an immutable audit record carrying the transaction ID, framework version, rules executed with their versions, risk factors, decision, timestamp, latency, state version and correlation ID.
backend/ Gradle multi-module, Java 21, Spring Boot 3 — modular monolith
transaction-api/ HTTP boundary, validation, correlation ID, OpenAPI
risk-engine/ Layers 1–2, deterministic in-path rules
risk-state/ Redis-backed pre-computed risk state
network-risk/ Layer 3, counterparty and network signals
external-enrichment/ Layer 4, asynchronous enrichment
post-settlement/ Layer 5, pattern detection and feedback loop
audit/ Immutable append-only decision trail
assessment-engine/ 12 categories x maturity 0–4, configuration-driven
benchmarks/ JMH microbenchmarks and load harness
frontend/ Next.js 14 App Router, TypeScript, Tailwind — static export
docs/framework/ The eleven methodology documents — the framework itself
specs/ Phase specs used during development
Modules for later phases are declared empty on purpose: the architecture is visible from day one, and later work adds code inside those names rather than reshaping the build.
Report a vulnerability privately — please do not open a public issue. The process, scope and disclosure expectations are in SECURITY.md.
Practices in this repository: secrets via environment variables only, synthetic data only, PII minimisation by design, dependency and secret scanning in CI. The fraud typologies this framework defends against are documented openly in threat-model.md — that is deliberate, since a control you cannot describe is a control you cannot assess.
The methodology comes out of building and operating payment and market infrastructure, not from a literature review. Two convictions shape it, and both are unusual enough to be worth stating plainly:
Resilience belongs inside a fraud framework. A fraud control that is unavailable is not a fraud control, and recovery time that scales with business volume is the failure mode nobody is watching. That is why growth-coupling.md exists as its own document rather than a footnote.
False positives are not an acceptable side effect. They are failed payments, and on an irrevocable rail they are the failure the customer actually experiences.
Historical professional experience — not a result of this repository. The growth-coupling thesis is informed by prior work on exchange infrastructure recovery time, where a startup sequence of roughly thirty minutes was reduced by approximately eighty percent through parallel processing, batch validation, and the removal of sequential dependencies. That work predates this project and is unrelated to the code here. Nothing in this repository has been benchmarked against it.
v1.0.0 — benchmark suite executed and published with environment fingerprints; repository quality sweep; dependency and secret scanning blocking in CI; hosted demo API.
v1.1 — operations dashboard and transaction explorer; interactive assessment wizard; additional typology detectors.
Known limitations today — synthetic data only; the external registry in Layer 4 is simulated; there is no authentication in the open core; the benchmark suite has not been run; the frontend has no automated tests.
Issues and pull requests are welcome. Start with CONTRIBUTING.md for the build, test and PR conventions, and CODE_OF_CONDUCT.md.
If you are assessing fraud controls on an instant-payment rail and something in the methodology is wrong, contradicted by a primary source, or missing — an issue saying so is the single most useful contribution.
| For | Where |
|---|---|
| Technical questions, bugs, methodology corrections | GitHub Issues |
| Professional enquiries | |
| Private correspondence | ronaldobinho@gmail.com |
| Security vulnerabilities | SECURITY.md — please do not use a public issue |
Apache-2.0. Use it, adapt it, ship it. Attribution appreciated; a note about what you learned assessing your own rail, more so.

{ "transactionId": "txn_demo_001", "correlationId": "3f8c…", "decision": "REVIEW", // ALLOW | REVIEW | DECLINE "riskScore": 0.42, // composite, in [0, 1] "latencyMs": 0.412, // measured in-path pipeline duration "latencyMicros": 412, "riskFactors": [ // highest contribution first { "code": "…", "layer": "…", "contribution": 0.00, "ruleVersion": "…" } ], "layerResults": { "…": { } }, // per-layer detail, ordered by layer "explanation": "…", "degraded": false, // true when a layer ran on incomplete input "frameworkVersion": "0.1.0-SNAPSHOT", "evaluatedAt": "…" }