Examples
Small, runnable projects that put Osprey inside a real workflow. Each one has the rules, a scenario, the integration code, and a script that starts a clean Osprey server and checks every decision. The output on this page is from a real run.
git clone https://github.com/opensource-finance/osprey.git && cd osprey && make examplesNeeds Go 1.26+, Python 3 and curl. No packages to install.
01Money-mule detection for payments
mode: detectionIn authorised push payment (APP) fraud, a criminal talks a victim into a bank transfer. The money lands in a mule account that forwards it to new payees within minutes and is left empty. The victim's payment looks normal on its own. The mule account shows the pattern.
How it was built
- Five rules, one signal each: new account, new payee, money in and out within the hour, balance emptied, and a Confirmation of Payee name mismatch.
- The payments service sends what it already knows (account age, new payee, balances) in metadata, and the CoP result in enrichment. Osprey counts the velocity itself.
- The four mule signals are .review rules: each one alone is normal, so each adds its weight of 0.2 to the score and four of them together reach the 0.7 alert threshold. The CoP mismatch is a .fail rule: it alerts on its own.
- A few lines of Python turn the decision into an action: ALRT holds the payment, NALT with reasons shows a scam warning, NALT without reasons releases it.
From the config
{
"id": "mule-pass-through",
"expression": "velocity_count >= 3",
"weight": 0.2,
"bands": [
{"lowerLimit": 1, "subRuleRef": ".review",
"reason": "3 or more payments in or out of this account in the last hour"},
{"lowerLimit": 0, "upperLimit": 1, "subRuleRef": ".pass",
"reason": "Normal activity"}
]
}Real output
loaded rule mule-new-account loaded rule mule-new-payee loaded rule mule-pass-through loaded rule mule-balance-drain loaded rule mule-cop-no-match pay-001 cust-alice -> landlord-ltd 1200 GBP NALT score 0.00 action RELEASE pay-002 cust-carol -> ext-supplier 3400 GBP ALRT score 0.40 action HOLD - Confirmation of Payee: the name does not match the account - First payment to this payee pay-003 cust-bob -> cust-mule 2500 GBP NALT score 0.20 action WARN - First payment to this payee pay-004 cust-mule -> ext-x1 800 GBP NALT score 0.40 action WARN - First payment to this payee - Sender account is less than 30 days old pay-005 cust-mule -> ext-x2 800 GBP NALT score 0.60 action WARN - 3 or more payments in or out of this account in the last hour - First payment to this payee - Sender account is less than 30 days old pay-006 cust-mule -> ext-x3 850 GBP ALRT score 0.80 action HOLD - 3 or more payments in or out of this account in the last hour - First payment to this payee - Payment empties more than 90% of the balance - Sender account is less than 30 days old OK: all 6 payments got the expected action
The victim gets a warning. The mule's first two transfers collect signals (0.40, then 0.60). The transfer that empties the account scores 0.80 and is held.
02Structuring detection for a money transfer service
mode: complianceMany reporting rules start at 10,000. Some customers split one large amount into several transfers just below it. Each transfer looks normal on its own. The pattern only shows across them.
How it was built
- Four rules: amount just below 10,000, three or more transfers in the hour, 25,000 or more moved in the hour, and a sanctions screening hit sent in enrichment.
- Two typologies group them. Structuring weighs the near-threshold rule at 0.5 and the two velocity rules at 0.25 each, with an alert threshold of 0.75. Sanctions alerts on one hit.
- In compliance mode the typologies decide, so every rule uses .review.
- On each alert the service reads GET /evaluations/{id} and writes a case file: which typology fired and what each rule contributed.
From the config
{
"id": "rem-typology-structuring",
"alertThreshold": 0.75,
"rules": [
{"ruleId": "rem-near-threshold", "weight": 0.5},
{"ruleId": "rem-repeat-sender", "weight": 0.25},
{"ruleId": "rem-hour-total", "weight": 0.25}
]
}Real output
loaded rule rem-near-threshold
loaded rule rem-repeat-sender
loaded rule rem-hour-total
loaded rule rem-sanctions-hit
loaded typology rem-typology-structuring
loaded typology rem-typology-sanctions
rem-001 cust-eve -> rcp-eve-family 450 USD
NALT score 0.00
rem-002 cust-frank -> rcp-notary 15000 USD
NALT score 0.00
rem-003 cust-dana -> rcp-dana-1 9500 USD
NALT score 0.50
- Amount is just below the 10,000 reporting threshold
rem-004 cust-dana -> rcp-dana-1 9400 USD
NALT score 0.50
- Amount is just below the 10,000 reporting threshold
rem-005 cust-dana -> rcp-dana-1 9700 USD
ALRT score 1.00
- 25,000 or more moved by this customer in the last hour
- 3 or more transfers by this customer in the last hour
- Amount is just below the 10,000 reporting threshold
case file:
typology rem-typology-structuring: score 1.00 >= threshold 0.75
rem-near-threshold rule score 1 x weight 0.50 = 0.50
rem-repeat-sender rule score 1 x weight 0.25 = 0.25
rem-hour-total rule score 1 x weight 0.25 = 0.25
rem-006 cust-gus -> rcp-flagged 200 USD
ALRT score 1.00
- Recipient matches a sanctions list entry
case file:
typology rem-typology-sanctions: score 1.00 >= threshold 1.00
rem-sanctions-hit rule score 1 x weight 1.00 = 1.00
OK: all 6 transfers got the expected decisionA single 15,000 transfer does not alert. Three cash transfers of about 9,500 within the hour do, and the case file shows the 0.50 + 0.25 + 0.25 behind the score.