[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Payments Reconciliation Engine

Build a Payments Reconciliation Engine

Automate daily reconciliation of payments across Stripe, PayPal, and bank statements — catching discrepancies in minutes instead of days and reducing month-end close from 5 days to 4 hours.

#postgresql#database#sql#relational#jsonb
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
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
>

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
>

bull-mq

v1.0.0

You are an expert in BullMQ, the high-performance job queue for Node.js built on Redis. You help developers build reliable background processing systems with delayed jobs, rate limiting, prioritization, repeatable cron jobs, job dependencies, concurrency control, and dead-letter handling — powering email sending, image processing, webhook delivery, report generation, and any async workload.

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
>

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
$

The Problem

A marketplace processes $5M/month through Stripe, PayPal, and direct bank transfers across 500 merchants. The finance team manually reconciles payments in spreadsheets — matching platform records against gateway reports against bank statements. It takes 5 days every month-end. Last quarter, they found $47K in discrepancies: 12 payments captured by Stripe but not recorded in the platform, 8 refunds processed but not reflected in merchant payouts, and 3 duplicate charges that went unnoticed for weeks.

Step 1: Normalized Transaction Schema

typescript
// src/reconciliation/schema.ts
import { z } from 'zod';

export const Transaction = z.object({
  id: z.string(),
  source: z.enum(['platform', 'stripe', 'paypal', 'bank']),
  externalId: z.string(),
  type: z.enum(['charge', 'refund', 'payout', 'fee', 'adjustment']),
  amountCents: z.number().int(),
  currency: z.string().length(3),
  merchantId: z.string().optional(),
  customerId: z.string().optional(),
  status: z.enum(['pending', 'completed', 'failed', 'disputed']),
  occurredAt: z.string().datetime(),
  metadata: z.record(z.string(), z.unknown()).default({}),
});

export type Transaction = z.infer<typeof Transaction>;

export const ReconciliationResult = z.object({
  date: z.string(),
  matched: z.number().int(),
  unmatched: z.array(z.object({
    source: z.string(),
    transaction: Transaction,
    possibleMatches: z.array(z.object({ source: z.string(), transaction: Transaction, confidence: z.number() })),
  })),
  discrepancies: z.array(z.object({
    type: z.enum(['amount_mismatch', 'missing_in_platform', 'missing_in_gateway', 'status_mismatch', 'duplicate']),
    platformTx: Transaction.optional(),
    gatewayTx: Transaction.optional(),
    amountDifferenceCents: z.number().int().optional(),
    severity: z.enum(['critical', 'high', 'medium', 'low']),
  })),
  summary: z.object({
    platformTotal: z.number().int(),
    gatewayTotal: z.number().int(),
    bankTotal: z.number().int(),
    netDiscrepancy: z.number().int(),
  }),
});

Step 2: Gateway Data Fetchers

typescript
// src/reconciliation/fetchers.ts
import Stripe from 'stripe';
import { Pool } from 'pg';
import type { Transaction } from './schema';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const db = new Pool({ connectionString: process.env.DATABASE_URL });

export async function fetchStripeTransactions(date: string): Promise<Transaction[]> {
  const startOfDay = new Date(`${date}T00:00:00Z`).getTime() / 1000;
  const endOfDay = new Date(`${date}T23:59:59Z`).getTime() / 1000;

  const charges = await stripe.charges.list({
    created: { gte: startOfDay, lte: endOfDay },
    limit: 100,
  });

  const refunds = await stripe.refunds.list({
    created: { gte: startOfDay, lte: endOfDay },
    limit: 100,
  });

  const transactions: Transaction[] = [];

  for (const charge of charges.data) {
    transactions.push({
      id: `stripe:${charge.id}`,
      source: 'stripe',
      externalId: charge.id,
      type: 'charge',
      amountCents: charge.amount,
      currency: charge.currency.toUpperCase(),
      merchantId: charge.metadata?.merchant_id,
      customerId: charge.customer as string,
      status: charge.status === 'succeeded' ? 'completed' : charge.disputed ? 'disputed' : 'failed',
      occurredAt: new Date(charge.created * 1000).toISOString(),
      metadata: charge.metadata ?? {},
    });
  }

  for (const refund of refunds.data) {
    transactions.push({
      id: `stripe:${refund.id}`,
      source: 'stripe',
      externalId: refund.id,
      type: 'refund',
      amountCents: -refund.amount,
      currency: refund.currency.toUpperCase(),
      status: refund.status === 'succeeded' ? 'completed' : 'pending',
      occurredAt: new Date(refund.created * 1000).toISOString(),
      metadata: {},
    });
  }

  return transactions;
}

export async function fetchPlatformTransactions(date: string): Promise<Transaction[]> {
  const { rows } = await db.query(`
    SELECT id, external_payment_id, type, amount_cents, currency,
           merchant_id, customer_id, status, created_at
    FROM payments
    WHERE created_at::date = $1::date
    ORDER BY created_at
  `, [date]);

  return rows.map(r => ({
    id: `platform:${r.id}`,
    source: 'platform' as const,
    externalId: r.external_payment_id,
    type: r.type,
    amountCents: r.amount_cents,
    currency: r.currency,
    merchantId: r.merchant_id,
    customerId: r.customer_id,
    status: r.status,
    occurredAt: r.created_at.toISOString(),
    metadata: {},
  }));
}

Step 3: Matching Engine

typescript
// src/reconciliation/matcher.ts
import type { Transaction, ReconciliationResult } from './schema';

export function reconcile(
  platformTxs: Transaction[],
  gatewayTxs: Transaction[]
): z.infer<typeof ReconciliationResult> {
  const matched: Array<[Transaction, Transaction]> = [];
  const unmatchedPlatform = new Set(platformTxs);
  const unmatchedGateway = new Set(gatewayTxs);
  const discrepancies: any[] = [];

  // Pass 1: exact match on external ID
  for (const ptx of platformTxs) {
    const match = gatewayTxs.find(g => g.externalId === ptx.externalId);
    if (match) {
      matched.push([ptx, match]);
      unmatchedPlatform.delete(ptx);
      unmatchedGateway.delete(match);

      // Check for amount mismatch
      if (ptx.amountCents !== match.amountCents) {
        discrepancies.push({
          type: 'amount_mismatch',
          platformTx: ptx,
          gatewayTx: match,
          amountDifferenceCents: Math.abs(ptx.amountCents - match.amountCents),
          severity: Math.abs(ptx.amountCents - match.amountCents) > 100 ? 'critical' : 'medium',
        });
      }

      // Check for status mismatch
      if (ptx.status !== match.status) {
        discrepancies.push({
          type: 'status_mismatch', platformTx: ptx, gatewayTx: match,
          severity: 'high',
        });
      }
    }
  }

  // Pass 2: fuzzy match remaining (amount + time window)
  for (const ptx of unmatchedPlatform) {
    const candidates = [...unmatchedGateway].filter(g =>
      Math.abs(g.amountCents - ptx.amountCents) <= 1 && // 1 cent tolerance
      Math.abs(new Date(g.occurredAt).getTime() - new Date(ptx.occurredAt).getTime()) < 86400000
    );
    if (candidates.length === 1) {
      matched.push([ptx, candidates[0]]);
      unmatchedPlatform.delete(ptx);
      unmatchedGateway.delete(candidates[0]);
    }
  }

  // Remaining unmatched = discrepancies
  for (const ptx of unmatchedPlatform) {
    discrepancies.push({ type: 'missing_in_gateway', platformTx: ptx, severity: 'high' });
  }
  for (const gtx of unmatchedGateway) {
    discrepancies.push({ type: 'missing_in_platform', gatewayTx: gtx, severity: 'critical' });
  }

  // Duplicate detection
  const seen = new Map<string, Transaction>();
  for (const tx of [...platformTxs, ...gatewayTxs]) {
    const key = `${tx.amountCents}:${tx.merchantId}:${tx.occurredAt.slice(0, 13)}`;
    if (seen.has(key)) {
      discrepancies.push({
        type: 'duplicate', platformTx: seen.get(key), gatewayTx: tx, severity: 'high',
      });
    }
    seen.set(key, tx);
  }

  return {
    date: new Date().toISOString().split('T')[0],
    matched: matched.length,
    unmatched: [],
    discrepancies,
    summary: {
      platformTotal: platformTxs.reduce((s, t) => s + t.amountCents, 0),
      gatewayTotal: gatewayTxs.reduce((s, t) => s + t.amountCents, 0),
      bankTotal: 0,
      netDiscrepancy: 0,
    },
  };
}

Results

  • Month-end close: reduced from 5 days to 4 hours
  • $47K discrepancy: would have been caught same-day (automated daily reconciliation)
  • 12 missing charges found in first run — Stripe webhooks had failed silently
  • Duplicate charges: caught within 1 hour instead of weeks
  • Finance team: 3 people freed from reconciliation work to focus on analysis
  • Audit readiness: complete reconciliation history with every transaction matched