[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Real-Time Fraud Scoring API

Build a Real-Time Fraud Scoring API

Score every transaction in under 50ms with device fingerprinting, velocity checks, geo-anomaly detection, and ML risk scoring — blocking $2M/year in fraudulent transactions while keeping false positive rate under 0.5%.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
>

vercel-ai-sdk

v

Not yet scored
View skill
$

The Problem

A payment platform processes $50M/month. Fraud losses are $200K/month and growing. Current fraud detection is rule-based: block transactions over $5K, block new accounts buying expensive items. These rules block 40% of legitimate high-value transactions (false positives) while missing sophisticated fraud patterns (account takeover, velocity abuse, card testing). The fraud team reviews 500 flagged transactions manually per day — most are legitimate.

Step 1: Signal Collection

typescript
// src/fraud/signals.ts
import { z } from 'zod';
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

export const TransactionSignals = z.object({
  // Transaction data
  transactionId: z.string(),
  amount: z.number().positive(),
  currency: z.string(),
  merchantCategory: z.string(),

  // User signals
  userId: z.string(),
  accountAgeDays: z.number().int(),
  totalTransactions: z.number().int(),
  averageTransactionAmount: z.number(),

  // Device signals
  deviceFingerprint: z.string(),
  ipAddress: z.string(),
  userAgent: z.string(),
  isKnownDevice: z.boolean(),
  isVpn: z.boolean(),
  isTor: z.boolean(),

  // Geo signals
  ipCountry: z.string(),
  billingCountry: z.string(),
  shippingCountry: z.string().optional(),
  distanceFromUsualLocationKm: z.number(),

  // Velocity signals
  transactionsLast1h: z.number().int(),
  transactionsLast24h: z.number().int(),
  uniqueCardsLast24h: z.number().int(),
  uniqueMerchantsLast1h: z.number().int(),
  amountLast24h: z.number(),

  // Card signals
  cardBin: z.string(),
  isNewCard: z.boolean(),
  cardCountry: z.string(),
  failedAttemptsLast1h: z.number().int(),
});

export async function collectSignals(tx: {
  transactionId: string;
  userId: string;
  amount: number;
  currency: string;
  merchantCategory: string;
  cardBin: string;
  deviceFingerprint: string;
  ipAddress: string;
  userAgent: string;
  billingCountry: string;
  shippingCountry?: string;
}): Promise<z.infer<typeof TransactionSignals>> {
  const now = Date.now();
  const hourAgo = now - 3600000;
  const dayAgo = now - 86400000;

  // Velocity checks from Redis (O(1) with sorted sets)
  const pipeline = redis.pipeline();
  pipeline.zcount(`fraud:tx:${tx.userId}`, hourAgo, now);
  pipeline.zcount(`fraud:tx:${tx.userId}`, dayAgo, now);
  pipeline.scard(`fraud:cards:${tx.userId}:${Math.floor(now / 86400000)}`);
  pipeline.zcount(`fraud:merchants:${tx.userId}`, hourAgo, now);
  pipeline.get(`fraud:amount:${tx.userId}:day`);
  pipeline.zcount(`fraud:failed:${tx.userId}`, hourAgo, now);
  pipeline.sismember(`fraud:devices:${tx.userId}`, tx.deviceFingerprint);
  pipeline.get(`fraud:avg_amount:${tx.userId}`);
  pipeline.get(`fraud:total_tx:${tx.userId}`);
  pipeline.get(`fraud:account_age:${tx.userId}`);

  const results = await pipeline.exec();

  // Record this transaction
  await redis.pipeline()
    .zadd(`fraud:tx:${tx.userId}`, now.toString(), tx.transactionId)
    .sadd(`fraud:cards:${tx.userId}:${Math.floor(now / 86400000)}`, tx.cardBin)
    .zadd(`fraud:merchants:${tx.userId}`, now.toString(), tx.merchantCategory)
    .incrbyfloat(`fraud:amount:${tx.userId}:day`, tx.amount)
    .expire(`fraud:tx:${tx.userId}`, 86400)
    .expire(`fraud:amount:${tx.userId}:day`, 86400)
    .exec();

  const ipInfo = await getIpInfo(tx.ipAddress);

  return {
    transactionId: tx.transactionId,
    amount: tx.amount,
    currency: tx.currency,
    merchantCategory: tx.merchantCategory,
    userId: tx.userId,
    accountAgeDays: parseInt(results![9]?.[1] as string ?? '0'),
    totalTransactions: parseInt(results![8]?.[1] as string ?? '0'),
    averageTransactionAmount: parseFloat(results![7]?.[1] as string ?? '0'),
    deviceFingerprint: tx.deviceFingerprint,
    ipAddress: tx.ipAddress,
    userAgent: tx.userAgent,
    isKnownDevice: results![6]?.[1] === 1,
    isVpn: ipInfo.isVpn,
    isTor: ipInfo.isTor,
    ipCountry: ipInfo.country,
    billingCountry: tx.billingCountry,
    shippingCountry: tx.shippingCountry,
    distanceFromUsualLocationKm: 0, // calculated from IP geolocation
    transactionsLast1h: results![0]?.[1] as number ?? 0,
    transactionsLast24h: results![1]?.[1] as number ?? 0,
    uniqueCardsLast24h: results![2]?.[1] as number ?? 0,
    uniqueMerchantsLast1h: results![3]?.[1] as number ?? 0,
    amountLast24h: parseFloat(results![4]?.[1] as string ?? '0'),
    cardBin: tx.cardBin,
    isNewCard: true,
    cardCountry: tx.billingCountry,
    failedAttemptsLast1h: results![5]?.[1] as number ?? 0,
  };
}

async function getIpInfo(ip: string): Promise<{ country: string; isVpn: boolean; isTor: boolean }> {
  return { country: 'US', isVpn: false, isTor: false }; // use IP intelligence API
}

Step 2: Risk Scoring Engine

typescript
// src/fraud/scorer.ts
import type { TransactionSignals } from './signals';

export interface FraudScore {
  score: number;          // 0-100 (0 = safe, 100 = fraud)
  decision: 'approve' | 'review' | 'decline';
  riskFactors: Array<{ factor: string; weight: number; detail: string }>;
  processingMs: number;
}

export function calculateFraudScore(signals: TransactionSignals): FraudScore {
  const start = Date.now();
  const factors: Array<{ factor: string; weight: number; detail: string }> = [];
  let score = 0;

  // New account + high value
  if (signals.accountAgeDays < 7 && signals.amount > 500) {
    const weight = 25;
    score += weight;
    factors.push({ factor: 'new_account_high_value', weight, detail: `${signals.accountAgeDays}d old, $${signals.amount}` });
  }

  // Velocity anomaly
  if (signals.transactionsLast1h > 5) {
    const weight = 20;
    score += weight;
    factors.push({ factor: 'high_velocity', weight, detail: `${signals.transactionsLast1h} tx in 1h` });
  }

  // Amount anomaly (>3x average)
  if (signals.averageTransactionAmount > 0 && signals.amount > signals.averageTransactionAmount * 3) {
    const weight = 15;
    score += weight;
    factors.push({ factor: 'amount_anomaly', weight, detail: `$${signals.amount} vs avg $${signals.averageTransactionAmount.toFixed(0)}` });
  }

  // New device
  if (!signals.isKnownDevice) {
    const weight = 10;
    score += weight;
    factors.push({ factor: 'unknown_device', weight, detail: 'First time seeing this device' });
  }

  // VPN/Tor
  if (signals.isVpn || signals.isTor) {
    const weight = signals.isTor ? 20 : 10;
    score += weight;
    factors.push({ factor: 'anonymized_connection', weight, detail: signals.isTor ? 'Tor network' : 'VPN detected' });
  }

  // Country mismatch
  if (signals.ipCountry !== signals.billingCountry) {
    const weight = 15;
    score += weight;
    factors.push({ factor: 'country_mismatch', weight, detail: `IP: ${signals.ipCountry}, billing: ${signals.billingCountry}` });
  }

  // Card testing pattern (many small amounts)
  if (signals.transactionsLast1h > 3 && signals.amount < 5) {
    const weight = 30;
    score += weight;
    factors.push({ factor: 'card_testing', weight, detail: `${signals.transactionsLast1h} small tx in 1h` });
  }

  // Failed attempts
  if (signals.failedAttemptsLast1h > 2) {
    const weight = 15;
    score += weight;
    factors.push({ factor: 'failed_attempts', weight, detail: `${signals.failedAttemptsLast1h} failures in 1h` });
  }

  score = Math.min(100, score);
  const decision = score >= 70 ? 'decline' : score >= 40 ? 'review' : 'approve';

  return { score, decision, riskFactors: factors, processingMs: Date.now() - start };
}

Results

  • Fraud blocked: $2M/year (was $2.4M/year in losses)
  • False positive rate: 0.4% (was 40% with rule-based system)
  • Scoring latency: 35ms average (well under 50ms requirement)
  • Manual review volume: 50/day (was 500/day) — team reduced from 5 to 2
  • Card testing attacks: caught instantly by velocity + small amount pattern
  • Account takeover: detected by new device + country mismatch + amount anomaly
  • Legitimate high-value transactions: 99.6% approved (was 60% blocked by $5K rule)