| name | audit-payment-system |
| description | Read-only audit for payment/money-movement systems, scope-gated so a simple Stripe-Checkout site and an in-house ledger/gateway each see only relevant findings. Checks the things that lose money or trigger PCI liability: idempotency on every mutation (double-charge on retry), double-entry append-only ledger, payment state machine (no double-capture), sync-auth vs async-webhook flow, HMAC + event-id webhook dedup, 3-way reconciliation vs PSP settlement, fraud/velocity + 3DS/SCA, multi-currency in minor units, PCI DSS v4.0.1 (never log PAN, tokens only, key rotation), and resilience (PSP timeout, partial ledger write, breaker). Uses the Stripe MCP for version-anchored provider checks when the PSP is Stripe. Use when "audit payment system", "payment gateway audit", "double charge / idempotency", "ledger / reconciliation", "webhook / 3DS / PCI", or "audit-payment-system". Defers per-call resilience to audit-resilience, PCI/secrets to audit-security, ledger schema to audit-db-schema. |
| license | MIT |
audit-payment-system — Money-Movement Correctness & Compliance Audit
Payment code fails differently from normal CRUD. A dropped PUT is an annoyance; a retried charge
is a double-charge, a lost ledger write is money that vanished, a logged PAN is PCI liability,
and an unverified webhook is a spoofed "payment succeeded". The bugs are silent — everything looks
green until a customer is charged twice or the month-end books don't balance. This audit checks the
controls that keep every cent accounted for and every charge exactly-once.
The 2026 consensus is consistent across sources: a payment system rests on three pillars —
idempotency, a double-entry ledger, and reconciliation — with PCI DSS v4.0.1 as a
non-negotiable floor and webhooks as the source of truth (never trust the synchronous API response
alone).
Read-only. This skill assesses and prioritizes; it does not change code. Payment code is a
STOP-and-confirm surface (see the composer execution rule) — findings here feed a human-reviewed
remediation, ideally with a stronger model, not an unattended edit. Delegations: per-call runtime
resilience → audit-resilience; PCI/secrets/authz/injection → audit-security; ledger &
partitioning schema → audit-db-schema; outbox/saga structure → audit-backend-architecture;
destructive/append-only integrity → plan-data-integrity; Stripe-specific integration →
the Stripe plugin skills. This skill owns the payment-domain correctness layer and links out rather
than duplicating.
Core principle — earn each control by scope; every gap is money or liability
Not every app needs an in-house double-entry ledger. A shop using Stripe Checkout offloads the ledger,
settlement, and most PCI scope to Stripe — flagging "no double-entry ledger" there is noise. But
idempotency, webhook verification, state sync, and tokens-only apply to everyone who moves money.
Gate depth by scope (Phase 0), then treat every in-scope gap by its blast radius: Critical = a
customer is charged twice, money is lost/unaccounted, or card data is exposed. There is no "low
severity" for a double-charge.
Phase 0 — Detect payment surfaces & scope (gates every later finding)
Find the money paths and the provider first; the scope tier decides which controls are in play. Never
report an in-house-ledger control as "Missing" on a pure merchant-integrator (N/A with a reason).
rg -n --hidden -g '!node_modules' -i "stripe|paypal|braintree|adyen|square|payjp|paypay|razorpay|checkout\.com|worldpay|mollie|@stripe/|payment_intent|paymentintent" -l
rg -n -i "\b(charge|capture|authoriz|refund|void|payout|settle|chargeback|dispute|reversal)\b" -l
rg -n -i "webhook|/webhooks?|constructEvent|verifyHeader|Stripe-Signature|x-signature|hmac" -l
rg -n -i "idempotenc|idempotency[-_]?key|Idempotency-Key" -l
rg -n -i "ledger|double[-_ ]entry|debit|credit|journal|balance|posting|book(keeping)?" -l
rg -n -i "reconcil|settlement|settle|payout report|balance_transaction|three[-_ ]way" -l
rg -n -i "amount|price|money|currency|minor[-_ ]unit|cents" -g '*.{ts,tsx,js,py,go,java,rb,cs,sql}' -l
rg -n -i "fraud|risk|velocity|3ds|3-?d ?secure|sca|radar|device.?fingerprint" -l
rg -n -i "card[-_ ]?number|\bpan\b|cvv|cvc|card\.number|primary_account" -l
Record a payment profile and pick the tier — apply only in-scope rows:
| Tier | Signals | In scope |
|---|
| P0 — Merchant integrator | uses hosted Checkout / PaymentIntents / a PSP SDK; PSP holds the money & ledger | Idempotency on mutations, webhook verify+dedup, payment-state sync (pull-based recovery), refund/void idempotency, tokens-only/PCI-SAQ scope, light recon vs PSP dashboard, resilience around PSP calls |
| P1 — Platform / marketplace | Connect-style split payments, payouts to sellers, multi-party balances | + payout/clawback saga, an internal ledger for balances owed, multi-party reconciliation, dispute→clawback flow |
| P2 — Gateway / PSP / wallet / fintech | own ledger, direct acquirer/bank/card-network, issues balances | + full double-entry append-only ledger, 3-way reconciliation (ledger↔settlement↔bank), settlement-file ingestion, sharding/serialized balance updates, in-house fraud engine, AML/sanctions, PCI DSS Level 1 |
If there is no money movement (no PSP, no charge/ledger paths), stop and report reduced
applicability. If card data appears in the last rg above, that is Critical, report immediately.
Phase 1 — Research (version-anchored, provider-aware)
Follow /research. Anchor to the installed SDK version and the provider's current API (e.g.
Stripe PaymentIntents, not the legacy Charges API, which lacks native SCA/3DS2). Confirm the
current-year shape of the controls before judging the code.
When the provider is Stripe, use the Stripe MCP as the authoritative source:
- Concepts / best practice (idempotency keys, webhook signature verification, PaymentIntents lifecycle,
SCA/3DS2, Radar) —
search_stripe_documentation with search_only_api_ref: false, e.g.
{ "question": "verify webhook signatures and prevent duplicate event processing", "language": "node" }.
- Exact API params the integration should be sending —
stripe_api_search (intent + resource) then
stripe_api_details on the operation id (e.g. confirm PaymentIntent.create is called with an
idempotency key and amounts in minor units).
For PayPal / Square / Adyen / PayPay / Braintree / others, the Stripe MCP does not apply — use
/research against the provider's official docs (idempotency header, webhook/HMAC verification, 3DS/SCA,
settlement report format). Never invent a param or endpoint the provider doesn't expose.
Phase 2 — Payment correctness matrix
For each in-scope row, mark Implemented / Partial / Missing / N/A with file:line, a one-line
"why it bites in prod", and the fix-delegate. Full detection commands, good-vs-red-flag signals, and
fix targets are in references/checklist.md — load it and work the applicable
groups.
A. Money-movement correctness (P0+)
| # | Control | Bites in prod if missing | Fix via |
|---|
| A1 | Idempotency on every mutation (charge/capture/refund/void) — key from business intent, enforced at gateway and a DB unique constraint | Network retry → double charge; the DB constraint is the last line of defense | audit-resilience, backend-patterns |
| A2 | Dedup / stored result — reused key returns the prior result; reused key + different payload is rejected | Retry runs the charge twice; or a bug reuses a key for a new amount | audit-resilience |
| A3 | Payment state machine — explicit permitted/prohibited transitions; capture-twice is idempotent (2nd returns success, no re-process); no SETTLED→AUTHORIZED, no re-capture of REFUNDED | Double-capture, refund-after-refund, stuck-in-limbo payments | backend-patterns |
| A4 | Money as integer minor units (never float); currency travels with amount | Float rounding silently loses/creates fractions of a cent at scale | audit-db-schema |
| A5 | Multi-currency & FX — no cross-currency arithmetic; FX rate captured at posting time; explicit rounding (e.g. bankers') | Mixed-currency sums, rounding drift, unreproducible historical amounts | backend-patterns |
B. Ledger & data integrity (P1 internal balances · P2 full ledger)
| # | Control | Bites in prod if missing | Fix via |
|---|
| B1 | Double-entry — every movement writes balanced debit+credit; sum of all entries = 0 (the invariant that proves nothing leaked) | Money "vanishes" or is created; books never balance; undetectable until audit | audit-db-schema, backend-patterns |
| B2 | Append-only / immutable transaction & ledger tables — corrections are reversing entries, never UPDATE/DELETE | An edited/deleted row destroys the audit trail; disputes become unwinnable | plan-data-integrity, audit-db-schema |
| B3 | Balance = derived, snapshotted separately — current balance is a snapshot/materialization of ledger entries, not a hand-updated column | Balance column drifts from the ledger; two sources of "truth" | audit-db-schema |
| B4 | Auditability — event-sourced/immutable history reconstructs any transaction; every access to txn data is logged | Can't answer "what happened to charge X"; fails compliance audit | backend-observability, audit-security |
| B5 | Schema for scale — partition by date (manageable rows/day), indexed for recon queries | Unbounded hot table; recon and reporting time out | audit-db-schema |
C. Async orchestration & webhook delivery (P0+)
| # | Control | Bites in prod if missing | Fix via |
|---|
| C1 | Sync-auth vs async-everything — authorization is synchronous; settlement, webhooks, reporting, recon are async | Slow downstream blocks the checkout; or status trusted from a response that lied | audit-backend-architecture |
| C2 | Webhook signature verified (HMAC / provider constructEvent) before any processing | Spoofed "payment succeeded" → goods shipped for free | audit-security |
| C3 | Webhook event-id dedup + 200-then-process — record processed event ids; ack 200 immediately, process async | PSP retries for days → the same event processed twice (double ledger post) | audit-resilience |
| C4 | Atomic state+ledger+outbox — the state transition, ledger posting, and outbound event commit in one DB transaction (outbox relay publishes) | Dual-write: crash mid-way = captured payment with no fulfillment event, or vice versa | audit-backend-architecture, backend-patterns |
| C5 | Pull-based recovery for stuck payments — a worker scans transitional states past a timeout and queries the PSP as source of truth | A lost webhook leaves a payment stuck forever; user re-tries → double charge | backend-patterns |
| C6 | Refund/dispute/payout as saga — multi-service steps with compensations (reverse auth, negative ledger entry, payout clawback, notify) | A half-done refund claws back money but never notifies, or refunds twice | backend-patterns |
D. Reconciliation & settlement (P1/P2)
| # | Control | Bites in prod if missing | Fix via |
|---|
| D1 | Automated daily reconciliation vs the PSP settlement file — the single most important control | Ledger and PSP silently diverge (timing, lost webhooks); discrepancies compound | backend-patterns, data-pipeline |
| D2 | 3-way match (internal ledger ↔ card-network/PSP ↔ bank statement) with a break report | Missing/extra/mismatched txns go unnoticed; revenue leakage & fraud hidden | data-pipeline |
| D3 | Discrepancy handling — missing txn escalated; extra bank txn found-or-reversed; amount mismatch checks FX; rounding-only auto-resolved | Every break needs a human; or breaks silently ignored | backend-patterns |
| D4 | Safety brake — unreconciled balance over a threshold halts new captures / alerts | Losses accumulate faster than they're caught | audit-resilience |
E. Fraud, risk & SCA (P0 delegate · P1/P2 in-house)
| # | Control | Bites in prod if missing | Fix via |
|---|
| E1 | Risk scoring pre-auth — velocity, geolocation, amount, device fingerprint via rules engine (+ ML score where present) | Card-testing / stolen-card attacks; chargebacks | backend-patterns |
| E2 | 3DS2 / SCA step-up — high-risk/PSD2-region → 3D Secure challenge; low-risk → frictionless via exemptions | Non-compliant in EU (declines) or friction everywhere (lost conversion) | provider docs / backend-patterns |
| E3 | Fraud-service failure policy — explicit fail-open vs fail-closed when the risk service is down (breaker) | Fraud service down → either block all revenue or wave through all fraud | audit-resilience |
| E4 | Chargeback / dispute monitoring — track ratio, react before acquirer watchlist (VAMP/VDMP) thresholds | Program placement / fines; account termination | backend-observability |
| E5 | AML / sanctions screening (P2 / regulated) | Regulatory exposure for regulated flows | /research + human |
F. Compliance & security — PCI DSS v4.0.1 (all tiers)
| # | Control | Bites in prod if missing | Fix via |
|---|
| F1 | Never store/log PAN or CVV — tokens only; card data never touches your servers/logs (scope reduction) | PCI breach liability; CVV storage is flatly prohibited | audit-security |
| F2 | Tokenization — hosted fields / PaymentIntents so raw card data bypasses your infra | Balloons PCI scope from SAQ-A to full audit | provider docs |
| F3 | Key rotation & secret handling — API/signing keys rotated, never in code/logs | Leaked long-lived key = unlimited charges/refunds | audit-security, plan-secrets-audit |
| F4 | Access audit — every read/write of transaction/PII data is logged & attributable | Can't prove who touched payment data; fails audit | audit-security, backend-observability |
G. Error handling & resilience (P0+)
| # | Control | Bites in prod if missing | Fix via |
|---|
| G1 | PSP/bank API timeout + retry with backoff and a circuit breaker | One slow provider exhausts the pool → whole checkout 503s | audit-resilience |
| G2 | Bulkhead / pool isolation — PSP calls can't starve the DB/other deps | Timeout storm cascades across the system | audit-backend-architecture |
| G3 | Partial-write safety — state + ledger commit atomically; no "charged but not booked" | Money taken, ledger never posted (or reverse) | backend-patterns |
| G4 | Graceful degradation for non-critical deps (fraud/notification down ≠ block auth, per policy) | A non-critical outage takes payments offline | audit-resilience |
Rules:
- Evidence or it didn't happen — every verdict cites
file:line or "searched, none found".
- N/A is first-class — record why (scope tier), don't drop the row.
- No double-counting — link per-call resilience to
audit-resilience, PCI to audit-security.
Phase 3 — Prioritized report (read-only)
## Payment System Audit — [repo] — [date]
**Provider(s):** [Stripe/PayPal/…] · **Scope tier:** [P0/P1/P2 + evidence]
**In scope:** [groups] · **N/A (out of tier):** [rows + why]
### Critical — money loss / double-charge / card-data exposure (fix before ship)
| Finding | Control | file:line | Why it bites | Fix via |
|---|---|---|---|---|
| Charge has no idempotency key; no unique constraint | A1 | pay/charge.ts:52 | Retry double-charges the customer | audit-resilience |
| Webhook processed without signature check | C2 | api/webhook.ts:9 | Spoofed "paid" → free goods | audit-security |
| Card number written to app log | F1 | pay/log.ts:20 | PCI breach liability | audit-security |
| DB write then broker publish (not atomic) | C4 | ledger.ts:88 | Captured, never booked → money unaccounted | backend-patterns |
### High / Medium (correctness & compliance matrix)
| Group | Implemented | Partial | Missing | N/A | Fix via |
|---|---|---|---|---|---|
| A Money-movement | … | … | … | | audit-resilience |
| B Ledger | … | … | … | (P0) | audit-db-schema |
| C Webhooks/async | … | … | … | | backend-patterns |
| D Reconciliation | … | … | … | (P0) | data-pipeline |
| E Fraud/SCA | … | … | … | | backend-patterns |
| F PCI/compliance | … | … | … | | audit-security |
| G Resilience | … | … | … | | audit-resilience |
### Lift-to-production roadmap (ordered by blast radius)
1. Idempotency (gateway + DB unique constraint) on every mutation → audit-resilience
2. Verify + dedup webhooks, 200-then-process async → audit-security / audit-resilience
3. Atomic state+ledger+outbox; pull-based recovery for stuck payments → backend-patterns
4. [P1/P2] Double-entry append-only ledger + daily reconciliation w/ break report → audit-db-schema / data-pipeline
5. Tokens-only + key rotation + access audit (PCI v4.0.1) → audit-security
6. Breaker/bulkhead + fraud fail-policy around every external call → audit-resilience
Forbidden: declaring "production-grade" from passing tests alone; flagging P2-only controls
(in-house ledger, 3-way bank match, sharding) against a P0 merchant integrator; re-auditing per-call
timeouts/retries audit-resilience owns; assigning any severity below Critical to a double-charge,
lost-money, or PAN-exposure finding; recommending a refund/payout saga without compensation logic;
editing payment code — this skill reports; remediation is human-reviewed (route to a stronger model
per the composer execution rule).
Related
audit-resilience — per-call idempotency keys, timeouts, retry+backoff+jitter, circuit breaker, cancellation
audit-security — PCI/PAN handling, webhook auth, secrets, key rotation, injection, access logging
audit-db-schema — double-entry ledger schema, append-only constraints, money types, partitioning
audit-backend-architecture — outbox/saga/breaker structure and topology fit (this skill checks payment correctness on top)
plan-data-integrity — append-only/immutability guarantees and destructive-op safety
plan-secrets-audit — rotate vs relocate provider API/signing keys
backend-patterns / backend-patterns/references/architecture-patterns.md — implement idempotency, outbox, saga, state machine
data-pipeline — reconciliation/settlement ingestion jobs
- the Stripe plugin skills (
stripe-best-practices, connect-recommend, upgrade-stripe) — Stripe-specific integration
complete-everything — close audited gaps to done with verification (human-reviewed for payment code)