- Introduction
- Getting Started
- Quick Start: Evaluating Transactions
- Related Documentation
- Core Concepts
- Basic Syntax
- Rule Structure
- Condition Types
- Actions and Verdicts
- Practical Examples
- Advanced Patterns
- Best Practices
- Common Patterns
- Troubleshooting
- Field Reference
- Conclusion
Blnk Watch is a domain-specific language (DSL) for creating real-time transaction monitoring rules.
It enables you to define conditions and automated actions for detecting fraud, enforcing limits, and staying compliant.
A Watch script is declarative: you describe what to detect and what action to take—the engine handles evaluation at runtime.
Note: This project is community-maintained. We welcome contributions and new maintainers!
# Build the binary
make build
# Build and install to GOPATH/bin
make install# From the project root
cd cmd/blnk-watch && go build -o ../../blnk-watch .Starts the main watch service with HTTP API on port 8081:
# Using the binary
./blnk-watch -command=watch
# Or using make
make watch
# With custom port
./blnk-watch -command=watch -port=9090
# With custom .env file
./blnk-watch -command=watch -env=.env.productionRuns continuous watermark synchronization from PostgreSQL to DuckDB:
# Using the binary
./blnk-watch -command=sync
# Or using make
make sync
# With custom sync interval and batch size
./blnk-watch -command=sync -sync-interval=5s -batch-size=500Performs a single watermark sync operation:
# Using the binary
./blnk-watch -command=sync-once
# Or using make
make sync-once
# With custom batch size
./blnk-watch -command=sync-once -batch-size=2000| Flag | Description | Default | Commands |
|---|---|---|---|
-command |
Command to run: watch, sync, or sync-once |
watch |
All |
-env |
Path to .env file | .env |
All |
-port |
Port for watch service HTTP server | 8081 |
watch |
-sync-interval |
Interval for watermark sync | 1s |
sync |
-batch-size |
Batch size for watermark sync | 1000 |
sync, sync-once |
Configure your .env file (see env.example) with:
DB_URL: PostgreSQL connection URLWATCH_SCRIPT_DIR: Directory for watch scripts (default:watch_scripts)WATCH_SCRIPT_GIT_REPO: Optional Git repository URL for watch scriptsWATCH_SCRIPT_GIT_BRANCH: Git branch to use (default:main)ALERT_WEBHOOK_*: Webhook alerting configuration (see below)
| Target | Description |
|---|---|
make build |
Build the binary |
make install |
Build and install to GOPATH/bin |
make watch |
Run the watch service |
make sync |
Run continuous watermark sync |
make sync-once |
Run one-time watermark sync |
make clean |
Remove built binaries |
make test |
Run tests |
make help |
Show help message |
Create a directory to store your watch rules:
mkdir watch_scriptsCreate a .ws file in the watch_scripts directory. Here's a simple rule to flag high-value transactions:
cat > watch_scripts/HighValueCheck.ws << 'EOF'
rule HighValueCheck {
description "Flag transactions over $10,000 for review"
when amount > 10000
then review
score 0.5
reason "Transaction amount exceeds $10,000"
}
EOFSee the examples/ directory for more rule templates covering velocity checks, blacklists, cross-border transactions, and more.
Send a transaction to the /inject endpoint for evaluation against your watch rules:
curl -X POST http://localhost:8081/inject \
-H "Content-Type: application/json" \
-d '{
"transaction_id": "txn_001",
"amount": 15000,
"currency": "USD",
"source": "acct_sender_123",
"destination": "acct_receiver_456",
"reference": "payment_ref_001",
"description": "Wire transfer",
"status": "pending",
"metadata": {
"kyc_tier": 1,
"source_country": "US",
"destination_country": "NG"
}
}'Response:
{
"transaction_id": "txn_001",
"message": "Call GET /transactions/{id} to fetch the processed transaction or set ALERT_WEBHOOK_URL to get webhooks when the transaction is flagged."
}After injecting a transaction, retrieve the full evaluation results:
curl http://localhost:8081/transactions/txn_001Response:
{
"transaction_id": "txn_001",
"amount": 15000,
"currency": "USD",
"source": "acct_sender_123",
"destination": "acct_receiver_456",
"reference": "payment_ref_001",
"description": "Wire transfer",
"status": "pending",
"metadata": {
"kyc_tier": 1,
"source_country": "US",
"destination_country": "NG",
"dsl_verdicts": [
{
"rule_id": 1,
"verdict": "review",
"score": 0.5,
"reason": "Amount exceeds $10,000"
}
],
"consolidated_risk_assessment": {
"final_risk_score": 0.5,
"final_verdict": "review",
"final_reason": "Amount exceeds $10,000",
"source_count": 1
}
}
}The metadata field contains:
dsl_verdicts: Array of individual rule evaluations withrule_id,verdict,score, andreasonconsolidated_risk_assessment: Aggregated risk assessment withfinal_risk_score,final_verdict,final_reason, andsource_count
To receive real-time alerts when transactions are flagged, configure webhooks in your .env file:
# Primary webhook URL for risk alerts
ALERT_WEBHOOK_URL=https://your-server.com/alerts
# Secondary webhook URL (fallback)
ALERT_WEBHOOK_SECONDARY_URL=https://backup.your-server.com/alerts
# Backup webhook URL (fallback)
ALERT_WEBHOOK_BACKUP_URL=https://another-backup.com/alerts
# API key for webhook authentication (sent as Bearer token)
ALERT_WEBHOOK_API_KEY=your_api_key_here
# Risk threshold for alerting (default: 0.5)
# Transactions with risk score >= this threshold trigger alerts
ALERT_WEBHOOK_RISK_THRESHOLD=0.5
# Enable/disable webhook alerting (set to "false" to disable)
ALERT_WEBHOOK_ENABLED=trueWebhook Payload:
When a transaction triggers an alert, the webhook receives:
{
"transaction_id": "txn_001",
"description": "Amount exceeds $10,000",
"risk_level": "medium",
"risk_score": 0.5,
"verdict": "review",
"source_count": 1,
"evaluation_data": {
"final_risk_score": 0.5,
"final_verdict": "review",
"final_reason": "Amount exceeds $10,000",
"source_count": 1,
"dsl_verdicts": [...],
"transaction_amount": 15000,
"transaction_reference": "payment_ref_001"
}
}Risk Levels:
high: Risk score >= 0.8medium: Risk score >= 0.6low: Risk score >= 0.3very_low: Risk score < 0.3
For more detailed technical documentation, see:
| Document | Description |
|---|---|
| API Documentation | Complete API reference for all endpoints |
| Watermark Sync | Transaction synchronization from PostgreSQL to DuckDB |
| Git Manager | Managing watch scripts via Git repositories |
| Parser Reference | DSL parser internals and grammar specification |
| Concept | Purpose |
|---|---|
| Rule | Named container that evaluates transactions |
| Condition | Logical test applied to transaction data |
| Action | Operation performed when a condition is true (block, review, alert, approve) |
| Aggregate | Time-window calculations such as sum, count, avg |
| Placeholder | Dynamic references like $current.source pointing to live transaction data |
- File extension:
.ws(Watch Script) - One rule per file
- Rules are compiled and hot-loaded automatically
rule RuleName {
description "What this rule checks"
when [conditions]
then [action]
score [0.0–1.0]
reason "Why the rule triggered"
}Key sections:
- rule … { }: Declares the rule
- description: Optional, but highly recommended
- when: Logical conditions joined by
and/or - then: Action block
rule HighValueTxn {
description "Flag transactions over $10,000"
when amount > 10000
then review
score 0.5
reason "Amount exceeds $10,000"
}Use PascalCase for rule names (HighValueTxn, VelocityDetection) to keep scripts readable.
Conditions are the heart of a rule.
Combine multiple tests with and/or and group with parentheses for clarity.
// Numeric
when amount > 10000
when amount <= 500
// String
when currency == "USD"
when status != "settled"when metadata.kyc_tier == 1
when metadata.merchant_category == "gambling"// Static
when destination in ("acct1", "acct2")
// Dynamic lists (system-provided)
when metadata.country in $high_risk_countrieswhen description regex "regex:(?i)(btc|bitcoin|crypto)"
when description not_regex "regex:^legit"when hour_of_day(timestamp) >= 21
when day_of_week(timestamp) in (0,6)
when month_of_year(timestamp) == 12Use sum, count, avg, min, max over time windows.
when sum(amount when source == $current.source, "PT24H") > 5000
when count(when destination == $current.destination, "PT1H") > 10Time window format:
PT1H= 1 hourPT30M= 30 minutesP1D= 1 dayP7D= 7 days
Detect sequences or repeated behavior.
when previous_transaction(
within: "PT1H",
match: { source: "$current.source", status: "failed" }
)Access current transaction context:
when sum(amount when source == $current.source, "PT24H") > 5000Actions determine what happens when a rule triggers.
| Action | Effect |
|---|---|
block |
Reject immediately |
review |
Hold for manual review |
alert |
Notify but allow |
approve |
Force approval |
Score (0.0–1.0) reflects risk level.
then block
score 1.0
reason "Account on sanctions list"Use scores consistently:
- 0.8–1.0: High confidence (block)
- 0.5–0.7: Suspicious (review)
- 0.1–0.4: Mild anomaly (alert only)
rule HighValueTransaction {
description "Review any transaction above $10,000"
when amount > 10000
then review
score 0.5
reason "Amount exceeds threshold"
}rule HighVelocitySpending {
description "Detect rapid spending from a single account"
when sum(amount where source == $current.source, "PT1H") > 5000
then review
score 0.7
reason "Spending velocity exceeded"
}rule BlockBlacklistedAccounts {
description "Stop transactions from blacklisted accounts"
when source in ("blocked_account1", "blocked_account2")
then block
score 1.0
reason "Source is blacklisted"
}rule StructuringDetection {
description "Detect multiple small deposits intended to evade limits"
when amount < 10000
and count(where source == $current.source, "PT24H") >= 3
and sum(amount where source == $current.source, "PT24H") > 25000
then review
score 0.8
reason "Possible structuring"
}rule AccountTakeoverPattern {
description "Detect suspicious access at odd hours"
when previous_transaction(
within: "P30D",
match: { source: "$current.source" }
)
and hour_of_day(timestamp) between 2 and 5
and amount > 5000
then review
score 0.9
reason "Potential account compromise"
}rule CrossBorderCompliance {
description "Enhanced checks for cross-border to high-risk jurisdictions"
when metadata.source_country != metadata.destination_country
and metadata.destination_country in $high_risk_countries
and amount > 1000
then review
score 0.6
reason "Cross-border transaction to high-risk country"
}-
Naming
Use PascalCase with purpose (
HighValueCheck, notRule1). -
Descriptive Comments
Always explain thresholds and intent in
description. -
Score Discipline
Reserve 1.0 for guaranteed fraud (e.g., sanction lists).
-
Threshold Setting
Start conservative and tune with live/ historical data.
-
Performance
- Keep aggregates narrow (avoid
P30Dwithout filters). - Filter inside aggregates (e.g.,
where source == $current.source). - Avoid complex regex where possible.
- Keep aggregates narrow (avoid
-
Testing
Test on historical data and monitor false positives.
| Category | Sample Condition |
|---|---|
| Amount | amount > 50000 |
| Frequency | count(where source == $current.source, "PT15M") > 5 |
| Time-based | day_of_week(timestamp) in (0,6) |
| Geographic | metadata.destination_country in $sanctioned_countries |
| Account Type | metadata.account_age_days < 30 and amount > 5000 |
| Issue | Fix |
|---|---|
| Rule not firing | Check field names, types, and time window |
| Syntax error | Verify quotes, operators (==), and parentheses |
| Slow evaluation | Narrow windows, add filters, simplify regex |
| High false positives | Adjust thresholds and add qualifying conditions |
- Start simple and expand incrementally
- Check compilation logs
- Replay historical transactions for testing
- Track false positive/negative rates in monitoring
Common transaction fields:
transaction_idamountcurrencysource/destinationdescriptiontimestampstatusmetadata.*(nested business fields like KYC tier, country, account age)
All field names are case-sensitive.
This project is community-maintained and we're actively looking for maintainers to help guide its development.
If you're interested in contributing as a maintainer, we'd love to have you! Areas where help is especially welcome:
- Code contributions: Bug fixes, feature additions, and improvements
- Documentation: Improving guides, examples, and API documentation
- Community support: Helping answer questions and review pull requests
- Testing: Writing tests, improving test coverage, and validating changes
- Project governance: Helping set direction, triage issues, and manage releases
To get started, check out open issues, submit a pull request, or reach out to discuss how you'd like to contribute.