.claude/skills/fintech-engineer/SKILL.md
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.
npx skillsauth add oimiragieo/agent-studio fintech-engineerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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
// 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 };
}
-- 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];
});
}
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',
});
}
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 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 })
);
// 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]))
);
}
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');
}
// 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
// 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,
});
// 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);
}
}
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.
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();
}
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'
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
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,
});
| 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.
0.1 + 0.2 !== 0.3)Date.now() for financial timestamps (use database server time)| 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 |
tools
Comprehensive biosignal processing toolkit for analyzing physiological data including ECG, EEG, EDA, RSP, PPG, EMG, and EOG signals. Use this skill when processing cardiovascular signals, brain activity, electrodermal responses, respiratory patterns, muscle activity, or eye movements. Applicable for heart rate variability analysis, event-related potentials, complexity measures, autonomic nervous system assessment, psychophysiology research, and multi-modal physiological signal integration.
tools
Comprehensive toolkit for creating, analyzing, and visualizing complex networks and graphs in Python. Use when working with network/graph data structures, analyzing relationships between entities, computing graph algorithms (shortest paths, centrality, clustering), detecting communities, generating synthetic networks, or visualizing network topologies. Applicable to social networks, biological networks, transportation systems, citation networks, and any domain involving pairwise relationships.
data-ai
Molecular featurization for ML (100+ featurizers). ECFP, MACCS, descriptors, pretrained models (ChemBERTa), convert SMILES to features, for QSAR and molecular ML.
development
Run Python code in the cloud with serverless containers, GPUs, and autoscaling. Use when deploying ML models, running batch processing jobs, scheduling compute-intensive tasks, or serving APIs that require GPU acceleration or dynamic scaling.