Agent Skills: Fintech Engineer Skill

Fintech engineering expertise — payment processing (Stripe, Plaid), PCI DSS compliance, financial data modeling (double-entry bookkeeping), fraud detection patterns, bank-grade security (encryption, tokenization), open banking APIs, cryptocurrency/blockchain integration, regulatory compliance (KYC/AML), and idempotent financial transaction design. Use for payment systems, banking apps, trading platforms, and fintech infrastructure.

UncategorizedID: oimiragieo/agent-studio/fintech-engineer

Install this agent skill to your local

pnpm dlx add-skill https://github.com/oimiragieo/agent-studio/tree/HEAD/.claude/skills/fintech-engineer

Skill Files

Browse the full folder contents for fintech-engineer.

Download Skill

Loading file tree…

.claude/skills/fintech-engineer/SKILL.md

Skill Metadata

Name
fintech-engineer
Description
Fintech engineering expertise — payment processing (Stripe, Plaid), PCI DSS compliance, financial data modeling (double-entry bookkeeping), fraud detection patterns, bank-grade security (encryption, tokenization), open banking APIs, cryptocurrency/blockchain integration, regulatory compliance (KYC/AML), and idempotent financial transaction design. Use for payment systems, banking apps, trading platforms, and fintech infrastructure.

Fintech Engineer Skill

Overview

Financial technology engineering covering payment processing, ledger design, compliance, security, and fintech API integration. Core principle: correctness over speed — financial bugs have real monetary consequences.

Critical Rules

IRON LAWS OF FINANCIAL ENGINEERING:
1. Monetary values = integers (cents/pence/satoshis) — NEVER floats
2. All writes are idempotent (idempotency keys on every mutation)
3. Double-entry bookkeeping — debits always equal credits
4. Audit log every financial event — immutable, append-only
5. Fail safe — on error, roll back fully or do nothing
6. Never store card PANs — use tokenization providers
7. Assume network failures — design for exactly-once delivery

Monetary Value Handling

// ALWAYS store as integer (smallest currency unit)
// NEVER use floating point for money

// BAD — floating point arithmetic errors
const price = 9.99;
const tax = price * 0.08; // 0.7992000000000001 — WRONG

// GOOD — integer arithmetic in cents
const priceInCents = 999; // $9.99
const taxInCents = Math.round(priceInCents * 0.08); // 80 cents = $0.80

// Currency formatting (display only — never compute with these)
function formatMoney(cents: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
  }).format(cents / 100);
}

// Money type for type safety
type Money = {
  amount: number; // Integer in smallest unit
  currency: string; // ISO 4217 (USD, EUR, GBP)
};

function addMoney(a: Money, b: Money): Money {
  if (a.currency !== b.currency) throw new Error('Currency mismatch');
  return { amount: a.amount + b.amount, currency: a.currency };
}

Double-Entry Ledger Design

-- Ledger accounts table
CREATE TABLE accounts (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type        TEXT NOT NULL CHECK (type IN ('asset', 'liability', 'equity', 'revenue', 'expense')),
  name        TEXT NOT NULL,
  currency    TEXT NOT NULL DEFAULT 'USD',
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Immutable ledger entries (double-entry)
CREATE TABLE ledger_entries (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transaction_id  UUID NOT NULL,          -- Groups debit+credit pairs
  account_id      UUID NOT NULL REFERENCES accounts(id),
  amount          BIGINT NOT NULL,         -- Positive = debit, Negative = credit
  currency        TEXT NOT NULL,
  description     TEXT,
  reference_type  TEXT,                   -- 'payment', 'refund', 'fee', etc.
  reference_id    TEXT,                   -- External ID (Stripe charge ID, etc.)
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  -- Ledger entries are NEVER updated or deleted
  CONSTRAINT no_zero_amount CHECK (amount != 0)
);

-- Account balance view (computed from ledger)
CREATE VIEW account_balances AS
SELECT
  account_id,
  currency,
  SUM(amount) AS balance
FROM ledger_entries
GROUP BY account_id, currency;
// Record a payment (debit cash, credit revenue)
async function recordPayment(
  db: Database,
  { userId, amountCents, currency, stripeChargeId, idempotencyKey }: PaymentParams
) {
  return db.transaction(async trx => {
    // Idempotency check
    const existing = await trx('transactions').where({ idempotency_key: idempotencyKey }).first();
    if (existing) return existing; // Return same result, do not double-process

    const txId = randomUUID();

    // Debit: cash/receivables account (asset increases)
    await trx('ledger_entries').insert({
      transaction_id: txId,
      account_id: CASH_ACCOUNT_ID,
      amount: amountCents, // Positive = debit
      currency,
      reference_type: 'payment',
      reference_id: stripeChargeId,
    });

    // Credit: revenue account (revenue increases = negative in double-entry)
    await trx('ledger_entries').insert({
      transaction_id: txId,
      account_id: REVENUE_ACCOUNT_ID,
      amount: -amountCents, // Negative = credit
      currency,
      reference_type: 'payment',
      reference_id: stripeChargeId,
    });

    // Record transaction with idempotency key
    const tx = await trx('transactions')
      .insert({
        id: txId,
        user_id: userId,
        idempotency_key: idempotencyKey,
        amount: amountCents,
        currency,
        status: 'completed',
        stripe_charge_id: stripeChargeId,
      })
      .returning('*');

    return tx[0];
  });
}

Stripe Integration

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
  typescript: true,
});

// Payment Intent (recommended flow)
async function createPaymentIntent(amountCents: number, currency: string, customerId: string) {
  return stripe.paymentIntents.create({
    amount: amountCents,
    currency,
    customer: customerId,
    automatic_payment_methods: { enabled: true },
    idempotency_key: `pi-${customerId}-${Date.now()}`, // Unique per attempt
    metadata: { orderId: 'order_123' },
  });
}

// Webhook handling (ALWAYS verify signature)
app.post('/webhook/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature']!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, // Raw body — do NOT parse as JSON first
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    res.status(400).send('Invalid signature');
    return;
  }

  // Idempotent event processing
  switch (event.type) {
    case 'payment_intent.succeeded': {
      const pi = event.data.object as Stripe.PaymentIntent;
      await handlePaymentSucceeded(pi);
      break;
    }
    case 'payment_intent.payment_failed': {
      const pi = event.data.object as Stripe.PaymentIntent;
      await handlePaymentFailed(pi);
      break;
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      await cancelSubscription(sub.id);
      break;
    }
  }

  res.json({ received: true });
});

// Refund
async function refundPayment(paymentIntentId: string, amountCents?: number) {
  return stripe.refunds.create({
    payment_intent: paymentIntentId,
    amount: amountCents, // Omit for full refund
    reason: 'requested_by_customer',
  });
}

Plaid Integration (Bank Account Linking)

import { PlaidApi, Configuration, PlaidEnvironments, Products, CountryCode } from 'plaid';

const plaid = new PlaidApi(
  new Configuration({
    basePath: PlaidEnvironments[process.env.PLAID_ENV as 'sandbox' | 'production'],
    baseOptions: {
      headers: {
        'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
        'PLAID-SECRET': process.env.PLAID_SECRET,
      },
    },
  })
);

// 1. Create Link token (server-side)
async function createLinkToken(userId: string) {
  const response = await plaid.linkTokenCreate({
    user: { client_user_id: userId },
    client_name: 'My App',
    products: [Products.Auth, Products.Transactions],
    country_codes: [CountryCode.Us],
    language: 'en',
  });
  return response.data.link_token;
}

// 2. Exchange public token for access token (after user completes Link)
async function exchangeToken(publicToken: string) {
  const response = await plaid.itemPublicTokenExchange({ public_token: publicToken });
  // Store access_token securely — this is permanent
  return response.data.access_token;
}

// 3. Fetch transactions
async function getTransactions(accessToken: string, startDate: string, endDate: string) {
  let allTransactions: Transaction[] = [];
  let hasMore = true;
  let offset = 0;

  while (hasMore) {
    const response = await plaid.transactionsGet({
      access_token: accessToken,
      start_date: startDate,
      end_date: endDate,
      options: { count: 500, offset },
    });
    allTransactions = [...allTransactions, ...response.data.transactions];
    hasMore = allTransactions.length < response.data.total_transactions;
    offset = allTransactions.length;
  }

  return allTransactions;
}

Idempotency Pattern

// Idempotency key middleware for financial APIs
async function idempotentOperation<T>(
  key: string,
  operation: () => Promise<T>,
  ttlSeconds = 86400 // 24 hours
): Promise<T> {
  const cached = await redis.get(`idempotency:${key}`);
  if (cached) {
    return JSON.parse(cached) as T;
  }

  const result = await operation();

  // Cache result — subsequent calls return same result
  await redis.set(`idempotency:${key}`, JSON.stringify(result), { EX: ttlSeconds });
  return result;
}

// Usage
const charge = await idempotentOperation(`charge:${orderId}:${userId}`, () =>
  stripe.charges.create({ amount: 9999, currency: 'usd', source: tokenId })
);

PCI DSS Compliance

// Card data — NEVER store, log, or transmit raw PANs
// Use Stripe Elements or similar to keep card data out of your systems

// WRONG — PCI violation:
// const cardNumber = req.body.cardNumber; // Never touches your server with Stripe Elements

// CORRECT — Stripe Elements flow:
// 1. Browser: stripe.createToken(cardElement) → returns { token: { id: 'tok_xxx' } }
// 2. Browser sends tok_xxx to your server
// 3. Server uses tok_xxx with Stripe API — never sees card data

// Masking for logs
function maskPAN(pan: string): string {
  return `****-****-****-${pan.slice(-4)}`;
}

// PCI-required: no card data in logs
function sanitizeForLogging(obj: Record<string, unknown>): Record<string, unknown> {
  const REDACT_FIELDS = ['card_number', 'cvv', 'pan', 'ssn', 'account_number'];
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => (REDACT_FIELDS.includes(k) ? [k, '[REDACTED]'] : [k, v]))
  );
}

Encryption for PII at Rest

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); // 32 bytes

function encryptPII(plaintext: string): { ciphertext: string; iv: string; tag: string } {
  const iv = randomBytes(12); // 96-bit IV for GCM
  const cipher = createCipheriv(ALGORITHM, KEY, iv);
  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  return {
    ciphertext: encrypted.toString('base64'),
    iv: iv.toString('base64'),
    tag: cipher.getAuthTag().toString('base64'),
  };
}

function decryptPII(ciphertext: string, iv: string, tag: string): string {
  const decipher = createDecipheriv(ALGORITHM, KEY, Buffer.from(iv, 'base64'));
  decipher.setAuthTag(Buffer.from(tag, 'base64'));
  return Buffer.concat([
    decipher.update(Buffer.from(ciphertext, 'base64')),
    decipher.final(),
  ]).toString('utf8');
}

KYC/AML Patterns

// Risk scoring
type RiskLevel = 'low' | 'medium' | 'high' | 'blocked';

interface KYCCheck {
  userId: string;
  identityVerified: boolean;
  pepMatch: boolean; // Politically Exposed Person
  sanctionsMatch: boolean; // OFAC, EU, UN sanctions lists
  countryRisk: 'low' | 'medium' | 'high';
  documentScore: number; // 0-100 from ID verification provider
}

function calculateRisk(check: KYCCheck): RiskLevel {
  if (check.sanctionsMatch) return 'blocked';
  if (check.pepMatch) return 'high';
  if (!check.identityVerified) return 'high';
  if (check.countryRisk === 'high') return 'medium';
  if (check.documentScore < 70) return 'medium';
  return 'low';
}

// Transaction monitoring — flag suspicious patterns
function flagSuspiciousTransaction(tx: Transaction): string[] {
  const flags: string[] = [];
  if (tx.amountCents > 1_000_000_00) flags.push('large_transaction'); // >$1M
  if (tx.amountCents === 999_99 || tx.amountCents === 9_999_99) flags.push('structuring_risk');
  if (tx.countryCode && HIGH_RISK_COUNTRIES.has(tx.countryCode)) flags.push('high_risk_country');
  return flags;
}

const HIGH_RISK_COUNTRIES = new Set(['KP', 'IR', 'SY', 'CU']); // OFAC restricted

Audit Logging

// Append-only audit log — never update, never delete
interface AuditEntry {
  id: string;
  timestamp: Date;
  actor: string; // userId or 'system'
  action: string; // 'payment.created', 'refund.issued', etc.
  resourceType: string; // 'payment', 'account', 'user'
  resourceId: string;
  before?: unknown; // State before change (for mutations)
  after?: unknown; // State after change
  ip?: string;
  userAgent?: string;
}

async function auditLog(entry: Omit<AuditEntry, 'id' | 'timestamp'>) {
  await db('audit_log').insert({
    id: randomUUID(),
    timestamp: new Date(),
    ...entry,
    before: entry.before ? JSON.stringify(entry.before) : null,
    after: entry.after ? JSON.stringify(entry.after) : null,
  });
}

// Usage
await auditLog({
  actor: userId,
  action: 'payment.created',
  resourceType: 'payment',
  resourceId: paymentId,
  after: { amount: 9999, currency: 'USD', status: 'completed' },
  ip: req.ip,
});

Subscription Billing

// Stripe subscriptions
async function createSubscription(customerId: string, priceId: string) {
  return stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    payment_behavior: 'default_incomplete', // Don't activate until payment confirmed
    expand: ['latest_invoice.payment_intent'],
  });
}

// Handle subscription lifecycle events
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  const status = subscription.status;
  // 'active', 'past_due', 'canceled', 'unpaid', 'trialing', 'paused'

  await db('subscriptions')
    .where({ stripe_id: subscription.id })
    .update({
      status,
      current_period_end: new Date(subscription.current_period_end * 1000),
      cancel_at_period_end: subscription.cancel_at_period_end,
    });

  if (status === 'past_due') {
    await sendPaymentFailedEmail(subscription.customer as string);
  }
}

Stripe Advanced Best Practices

Stripe API Version Pinning

Always pin the API version in the Stripe client constructor. Never rely on the account default:

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia', // Pin explicitly — never omit
  typescript: true,
  telemetry: false, // Disable telemetry in production if desired
});

Upgrade API versions deliberately in a test environment. Breaking changes in Stripe API versions can silently corrupt payment flows.

Webhook Idempotency with Event ID Deduplication

Store processed webhook event IDs to prevent double-processing on retries:

async function processWebhookEvent(event: Stripe.Event) {
  // Check if already processed
  const processed = await db('webhook_events').where({ stripe_event_id: event.id }).first();
  if (processed) {
    console.log(`Skipping duplicate event: ${event.id}`);
    return { status: 'already_processed' };
  }

  // Process the event
  await handleEvent(event);

  // Mark as processed (with upsert for race safety)
  await db('webhook_events')
    .insert({
      stripe_event_id: event.id,
      event_type: event.type,
      processed_at: new Date(),
    })
    .onConflict('stripe_event_id')
    .ignore();
}

Radar Fraud Rules (Stripe Radar)

Configure Stripe Radar rules for fraud detection. Use metadata to pass risk signals:

// Pass risk metadata to Radar
const paymentIntent = await stripe.paymentIntents.create({
  amount: amountCents,
  currency,
  customer: customerId,
  metadata: {
    user_account_age_days: String(accountAgeDays),
    is_first_purchase: String(isFirstPurchase),
    shipping_matches_billing: String(shippingMatchesBilling),
    risk_score: String(computedRiskScore),
  },
});

// Radar rule example (configured in Stripe Dashboard):
// Block if: :risk_level: = 'high' AND :metadata.is_first_purchase: = 'true'

Strong Customer Authentication (SCA) for PSD2

For European payments, handle SCA challenges properly:

// On frontend: handle requires_action status
const { paymentIntent, error } = await stripe.confirmCardPayment(clientSecret);

if (paymentIntent?.status === 'requires_action') {
  // Stripe.js handles 3DS challenge automatically
  // Server webhook will fire payment_intent.succeeded when complete
}

// Never mark order as paid until webhook payment_intent.succeeded fires
// Frontend confirmation is NOT authoritative

Connect Platform Patterns

For marketplace/platform Stripe Connect:

// Create charge on behalf of connected account
const charge = await stripe.charges.create(
  {
    amount: 10000, // $100.00
    currency: 'usd',
    source: token,
    application_fee_amount: 500, // $5.00 platform fee
  },
  {
    stripeAccount: connectedAccountId, // 'acct_xxx'
  }
);

// Transfer to connected account (separate transfers model)
const transfer = await stripe.transfers.create({
  amount: 9500, // $100 - $5 fee
  currency: 'usd',
  destination: connectedAccountId,
  transfer_group: orderId,
});

Stripe Testing Checklist

| Scenario | Test Card | Expected | | ------------------ | --------------------- | --------------------------------- | | Successful payment | 4242 4242 4242 4242 | payment_intent.succeeded | | Declined card | 4000 0000 0000 0002 | payment_intent.payment_failed | | Requires 3DS | 4000 0025 0000 3155 | requires_action → 3DS → success | | Insufficient funds | 4000 0000 0000 9995 | card_declined | | Expired card | 4000 0000 0000 0069 | expired_card |

Always test webhook delivery with stripe listen --forward-to localhost:3000/webhook during development.

Anti-Patterns

  • Floating-point arithmetic for monetary values (0.1 + 0.2 !== 0.3)
  • Non-idempotent financial mutations (double charges on retry)
  • Updating or deleting ledger entries (they must be immutable)
  • Storing card PANs, CVVs, or magnetic stripe data
  • Logging full request bodies in payment flows (may contain card data)
  • Skipping webhook signature verification
  • Using Date.now() for financial timestamps (use database server time)
  • Rollback without audit log entry (must log failed attempts too)
  • Currency conversion at ingestion time (store in original currency, convert at display)

Regulatory References

| Regulation | Scope | Key Requirements | | ----------- | ---------------- | -------------------------------------------------------- | | PCI DSS 4.0 | Card payments | Encrypt PANs, tokenize, audit logs, penetration testing | | GDPR | EU users | Right to erasure, data minimization, breach notification | | PSD2 | EU payments | Strong Customer Authentication (SCA), Open Banking APIs | | SOX | Public companies | Financial controls, audit trails, immutable records | | BSA/AML | US transactions | KYC, CTR >$10K, SAR filing, sanctions screening | | CCPA | California users | Data access rights, opt-out of sale |

Related