Fraud Rules Engine Design for B2B 2026: Velocity, BIN Scoring, Fingerprinting
B2B fraud in digital goods is different from B2C: lower transaction count, but higher average ticket ($200–2000), and a chargeback costs you the product, the interchange fee, and a dispute fee. This article is the blueprint for a fraud rules engine that catches 70%+ of fraud with <0.5% false positives.
The fraud engine lives entirely in your system and runs before you call POST /api/orders on FoxReload. Once an order is placed and fulfilled, the codes are delivered — your fraud layer is the gate that decides whether to call the API at all.
1. Velocity rules — the foundation
Velocity = frequency of actions per time window. The baseline set:
type FraudSignal = { rule: string; score: number; reason: string };
async function checkVelocity(order: Order): Promise<FraudSignal[]> {
const signals: FraudSignal[] = [];
const ipCount = await redis.zcount(`vel:ip:${order.ip}`, Date.now() - 600_000, '+inf');
if (ipCount > 5) signals.push({ rule: 'ip_velocity', score: 80, reason: '>5 orders/10min' });
const cards = await redis.scard(`vel:cards:${order.userId}:24h`);
if (cards > 3) signals.push({ rule: 'multi_card', score: 60, reason: '>3 cards/24h' });
return signals;
}
Storage: Redis sorted sets with TTL = window. Write: ZADD vel:ip:1.2.3.4 NOW order_id + EXPIRE 600.
2. BIN risk scoring
The first 6–8 digits of a card number (BIN) identify the issuing bank. Most fraud vendors maintain a BIN risk table:
const binRisk = await binLookup(card.bin); // 0..100
if (binRisk > 70) flags.push({ rule: 'high_risk_bin', score: 50, reason: `BIN ${card.bin}` });
if (binRisk === 100) return reject('blocked_bin'); // prepaid mass-issuance cards
High-risk BINs are prepaid cards (especially mass-issuance non-bank), cards from sanctioned jurisdictions, and BIN ranges seen in recent fraud waves. Refresh the table weekly.
3. Device fingerprinting
Cookie-less fingerprint (fpjs, ClientJS):
import FingerprintJS from '@fingerprintjs/fingerprintjs-pro';
const fp = await FingerprintJS.load({ apiKey: process.env.FPJS_KEY });
const result = await fp.get();
// result.visitorId — stable hash, ~99% accuracy
const fpHistory = await db.fingerprints.find({ visitorId: result.visitorId });
if (fpHistory.chargebackCount > 0) flag.score += 90;
This links "one real device — many accounts" and catches mass-account-creation fraud.
4. Comparison: Sift, Sumsub, in-house
| Provider | Type | Cost/transaction | Setup | Accuracy |
|---|---|---|---|---|
| In-house JSON rules | Rules | $0 | 1–2 weeks | 60–70% |
| Sift | ML-as-service | $0.04 | 1 day | 85–92% |
| Sumsub | KYC + fraud | $0.50–1.50 | 3 days | 80–88% |
| Riskified | Chargeback guarantee | 0.8–1.2% volume | 2 weeks | 90%+ |
| Stripe Radar | Built into payments | 0.5%/decision | 0 | 75–85% |
Recommendation for FoxReload partners: up to $500k/mo — in-house rules + Stripe Radar. Above $500k/mo — add Sift or Riskified. Take Sumsub only if you also need KYC.
Integration point with FoxReload
Run your fraud checks before calling POST /api/orders. A typical gate:
async function processOrder(customerOrder: CustomerOrder) {
const signals = await checkVelocity(customerOrder);
const score = signals.reduce((s, sig) => s + sig.score, 0);
if (score >= 100) {
return { decision: 'reject', reasons: signals };
}
if (score >= 50) {
await queueForManualReview(customerOrder);
return { decision: 'hold' };
}
// Safe to proceed — call FoxReload
const order = await fetch('https://public-api.foxreload.com/api/orders', {
method: 'POST',
headers: { 'X-API-Key': process.env.FOXRELOAD_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ itemId: customerOrder.productId, quantity: customerOrder.qty }],
}),
});
return { decision: 'approved', order: await order.json() };
}
Your fraud engine is the first line of defence. FoxReload processes what you send — the risk filtering is yours to own. Get access.
