Payment risk scoring for demo transactions.
Given a txn + a bit of user context, the engine returns allow, review, or block, plus which rules fired. Python owns the rules. Node owns the HTTP edge so a JS app can call it without rewriting risk logic in JS.
I built this after reading a few fintech fraud writeups and realizing most of the first pass is still: velocity, new device, amount vs baseline, geo hops — not a 50-layer network.
Each rule either returns nothing or a hit { rule_id, points, reason }.
Engine sums points (max 100) and maps:
| Score | Action |
|---|---|
| 0–24 | allow |
| 25–54 | review |
| 55+ | block |
Rules currently in scorer/rules.py:
- high_amount — spend vs user's average
- velocity_burst — too many txns in the last hour
- night_owl — big amount in the quiet UTC hours
- new_device — first time device + non-trivial amount
- geo_hop — multiple countries in 24h
- risky_combo — new device + web + high amount stacked
They're independent functions. You can comment one out without the rest collapsing.
flowchart TD
subgraph inputs
APP[Payment app / batch file]
FILE[transactions.json]
end
subgraph node [Node.js api/]
S[server.js POST /score]
H[score_file.js]
B[engine_bridge.js]
S --> B
H --> B
end
subgraph py [Python scorer/]
C[cli.py]
E[engine.py]
R[rules.py]
C --> E --> R
end
APP --> S
FILE --> H
FILE --> C
B -->|JSON on stdin| C
E --> D{score}
D -->|low| ALLOW[allow]
D -->|mid| REVIEW[review]
D -->|high| BLOCK[block]
fraud-pulse/
scorer/
rules.py # each rule is a small function
engine.py # sum + action mapping
cli.py # stdin/file → JSON decisions
api/
server.js
engine_bridge.js
score_file.js
sample_data/transactions.json
tests/
git clone https://github.com/Nabil201-ctrl/fraud-pulse.git
cd fraud-pulsePython path (fastest to see decisions):
python3 scorer/cli.py -i sample_data/transactions.jsonNode helper:
cd api
node score_file.js ../sample_data/transactions.jsonHTTP:
cd api
node server.js
# port 3850
curl -s -X POST http://127.0.0.1:3850/score \
-H 'content-type: application/json' \
-d @../sample_data/transactions.jsonChecks:
python3 -c "
import sys
from pathlib import Path
sys.path.insert(0, str(Path('scorer').resolve()))
sys.path.insert(0, str(Path('tests').resolve()))
from test_engine import test_normal_allow, test_obvious_fraud_blocks
test_normal_allow(); test_obvious_fraud_blocks()
print('ok')
"| txn | rough story | expected |
|---|---|---|
| t_1001 | normal coffee spend | allow |
| t_1002 | huge amount, new device, velocity, multi-country | block |
| t_1003 | elevated but not insane | review |
Open the JSON if you want to tweak fields and re-run — that's how I tuned the thresholds.
- Reasons are mandatory. If a rule adds points, it has to say why in plain language. Compliance people hate bare scores.
- Context on the payload. The engine doesn't fetch user history; the caller passes
user_avg_amount,user_txn_count_1h, etc. Keeps this repo self-contained. - Two languages, one contract. Node never reimplements rules.
- Pull features from a real event store instead of denormalized fields
- Slack/webhook on
block - Time-decay on velocity windows