Finance
reconciliation-patterns - Claude MCP Skill
Patterns for syncing state between external services (Stripe, Clerk) and local database. Invoke for: webhook failures, data sync issues, eventual consistency, recovery from missed events, subscription state management.
SEO Guide: Enhance your AI agent with the reconciliation-patterns tool. This Model Context Protocol (MCP) server allows Claude Desktop and other LLMs to patterns for syncing state between external services (stripe, clerk) and local database. invoke for:... Download and configure this skill to unlock new capabilities for your AI workflow.
Documentation
SKILL.md# Reconciliation Patterns
Patterns for maintaining data consistency between external services and your database when webhooks fail or events are missed.
## The Problem
External services (Stripe, Clerk, etc.) notify your app via webhooks. But webhooks can:
- Fail silently (wrong URL, network issues)
- Be delivered out of order
- Be duplicated
- Miss events entirely
**Result**: Your database state diverges from source of truth.
## Core Principle
**Webhooks for speed, reconciliation for correctness.**
1. Process webhooks for real-time updates (optimistic)
2. Run periodic reconciliation to catch and fix drift (defensive)
## Pattern 1: Scheduled Reconciliation
Run cron job to compare local state with external service.
```typescript
// Convex scheduled function
export const reconcileSubscriptions = internalAction({
handler: async (ctx) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// Get all active subscriptions from Stripe
const stripeSubscriptions = await stripe.subscriptions.list({
status: 'all',
limit: 100,
})
// Get all users from our database
const users = await ctx.runQuery(internal.users.listWithStripeId)
for (const user of users) {
const stripeSub = stripeSubscriptions.data.find(
(s) => s.customer === user.stripeCustomerId
)
const expectedStatus = stripeSub?.status ?? 'none'
if (user.subscriptionStatus !== expectedStatus) {
console.log(`Drift detected: user ${user._id}`, {
local: user.subscriptionStatus,
stripe: expectedStatus,
})
await ctx.runMutation(internal.users.updateSubscriptionStatus, {
userId: user._id,
status: expectedStatus,
subscriptionId: stripeSub?.id,
})
}
}
},
})
// Schedule: Run every hour
// crons.ts
export default {
reconcileSubscriptions: {
schedule: "0 * * * *", // Every hour
handler: internal.reconciliation.reconcileSubscriptions,
},
}
```
## Pattern 2: On-Demand Reconciliation
Reconcile specific user when they report issues.
```typescript
export const reconcileUser = action({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const user = await ctx.runQuery(internal.users.get, { id: args.userId })
if (!user?.stripeCustomerId) return { status: "no_stripe_customer" }
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// Fetch current state from Stripe
const customer = await stripe.customers.retrieve(user.stripeCustomerId, {
expand: ['subscriptions'],
})
if (customer.deleted) {
await ctx.runMutation(internal.users.clearSubscription, { userId: args.userId })
return { status: "customer_deleted" }
}
const subscription = customer.subscriptions?.data[0]
const stripeStatus = subscription?.status ?? 'none'
if (user.subscriptionStatus !== stripeStatus) {
await ctx.runMutation(internal.users.updateSubscriptionStatus, {
userId: args.userId,
status: stripeStatus,
subscriptionId: subscription?.id,
})
return {
status: "fixed",
was: user.subscriptionStatus,
now: stripeStatus,
}
}
return { status: "already_synced" }
},
})
```
## Pattern 3: Event Replay
Fetch and replay missed events from Stripe.
```typescript
export const replayMissedEvents = internalAction({
args: {
since: v.number(), // Unix timestamp
eventTypes: v.array(v.string()),
},
handler: async (ctx, args) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const events = await stripe.events.list({
created: { gte: args.since },
types: args.eventTypes,
limit: 100,
})
for (const event of events.data) {
// Check if we already processed this event
const existing = await ctx.runQuery(internal.events.findByStripeId, {
stripeEventId: event.id,
})
if (existing) {
console.log(`Event ${event.id} already processed, skipping`)
continue
}
// Process the event
await ctx.runAction(internal.webhooks.processStripeEvent, {
event: event,
})
}
return { processed: events.data.length }
},
})
```
## Pattern 4: Idempotent Webhook Handler
Ensure webhooks can be safely replayed.
```typescript
export const handleStripeWebhook = action({
args: { event: v.any() },
handler: async (ctx, args) => {
const event = args.event
// Check idempotency
const existing = await ctx.runQuery(internal.events.findByStripeId, {
stripeEventId: event.id,
})
if (existing) {
console.log(`Duplicate event ${event.id}, returning early`)
return { status: "duplicate" }
}
// Record event before processing
await ctx.runMutation(internal.events.record, {
stripeEventId: event.id,
type: event.type,
processedAt: Date.now(),
})
// Process based on event type
switch (event.type) {
case 'customer.subscription.updated':
await handleSubscriptionUpdate(ctx, event.data.object)
break
case 'invoice.paid':
await handleInvoicePaid(ctx, event.data.object)
break
// ... other events
}
return { status: "processed" }
},
})
```
## When to Reconcile
### Scheduled (Cron)
- **Hourly**: High-value data (subscriptions, payments)
- **Daily**: User profiles, preferences
- **Weekly**: Historical data, analytics
### On-Demand
- User reports "my subscription isn't showing"
- Support escalation
- After incident recovery
### Event-Triggered
- After webhook failure alert
- After deployment (reconcile during quiet period)
- When dashboard shows `pending_webhooks > 0`
## Best Practices
### Do
- **Log all drift detected** with before/after values
- **Store event IDs** for idempotency
- **Paginate** when fetching from external APIs
- **Rate limit** reconciliation to avoid API limits
- **Alert on significant drift** (e.g., >5% mismatch)
### Don't
- **Don't trust local state** as source of truth for external service data
- **Don't skip idempotency** checks
- **Don't reconcile too frequently** (API rate limits)
- **Don't ignore failed reconciliations** (alert and investigate)
## Debugging Drift
```typescript
// Diagnostic query: Find users with stale subscription data
export const findDriftedUsers = internalQuery({
handler: async (ctx) => {
const users = await ctx.db.query("users").collect()
return users.filter((u) => {
// Users with subscription but no Stripe ID
if (u.subscriptionStatus === 'active' && !u.stripeSubscriptionId) {
return true
}
// Users with lastSyncedAt > 24 hours ago
if (u.lastSyncedAt && Date.now() - u.lastSyncedAt > 86400000) {
return true
}
return false
})
},
})
```
## References
- `references/stripe-reconciliation.md` — Stripe-specific patterns
- `references/clerk-reconciliation.md` — Clerk user sync patterns
- `references/monitoring.md` — Alerting on drift
## Related Skills
- `stripe-best-practices` — Stripe integration patterns
- `clerk-auth` — Clerk authentication integration
- `verify-fix` — Incident verification protocolSignals
Information
- Repository
- phrazzld/claude-config
- Author
- phrazzld
- Last Sync
- 3/2/2026
- Repo Updated
- 3/1/2026
- Created
- 1/18/2026
Reviews (0)
No reviews yet. Be the first to review this skill!
Related Skills
upgrade-nodejs
Upgrading Bun's Self-Reported Node.js Version
cursorrules
CrewAI Development Rules
cn-check
Install and run the Continue CLI (`cn`) to execute AI agent checks on local code changes. Use when asked to "run checks", "lint with AI", "review my changes with cn", or set up Continue CI locally.
CLAUDE
CLAUDE.md
Related Guides
Bear Notes Claude Skill: Your AI-Powered Note-Taking Assistant
Learn how to use the bear-notes Claude skill. Complete guide with installation instructions and examples.
Mastering tmux with Claude: A Complete Guide to the tmux Claude Skill
Learn how to use the tmux Claude skill. Complete guide with installation instructions and examples.
OpenAI Whisper API Claude Skill: Complete Guide to AI-Powered Audio Transcription
Learn how to use the openai-whisper-api Claude skill. Complete guide with installation instructions and examples.