audit-payment-system — Money-Movement Correctness & Compliance Audit
Degree of freedom: MIXED — Scope, matrix, and severity [HIGH freedom];
Phase 0 detection searches [LOW freedom — run exactly]. Do not write
exploit or payment-fraud PoCs — quote the missing control, never a replay
or spoof recipe.
Read-only. Assess and prioritize; do not change code. Payment code is a
STOP-and-confirm surface — findings feed a human-reviewed remediation, ideally
with a stronger model. Delegations: per-call resilience → audit-resilience;
PCI/secrets/authz → audit-security; ledger schema → audit-db-schema;
outbox/saga structure → audit-backend-architecture; append-only
integrity → plan-data-integrity; Stripe integration → the Stripe plugin
skills. This skill owns payment-domain correctness.
Payment failures are silent: a retried charge is a double-charge, a lost ledger
write is vanished money, a logged PAN is PCI liability, an unverified webhook
is an untrusted "paid". Three pillars: idempotency, a double-entry
ledger (when in scope), reconciliation — plus PCI DSS v4.0.1 and
webhooks as source of truth (never trust the sync API response alone).
Core principle — earn each control by scope; every gap is money or liability
Not every app needs an in-house double-entry ledger. Stripe Checkout offloads
ledger, settlement, and most PCI — flagging "no double-entry ledger" there is
noise. Idempotency, webhook verification, state sync, and tokens-only apply
to everyone who moves money. Gate depth by scope (Phase 0). Critical = a
customer is charged twice, money is lost/unaccounted, or card data is
exposed. There is no "low severity" for a double-charge.
How to reason — Observe → Interpret → Classify → Severity
- Observe — quote the mutation/webhook/ledger
file:line (or "searched, none found")
- Interpret — what money or liability path breaks if that control is missing?
- Classify — Implemented / Partial / Missing / N/A (tier reason); control id (A1–G4)
- Severity — double-charge, lost money, or PAN/CVV exposure = Critical; P2-only rows are N/A on P0
Worked example
Observe: P0 Stripe Checkout. POST /api/checkout calls
paymentIntents.create with no idempotency key and no unique business-intent
constraint (app/api/checkout/route.ts). Webhook handler updates order status
from the parsed body without constructEvent / signature verification
(app/api/stripe/route.ts).
Interpret: a network retry can create two PaymentIntents for one checkout;
an unverified webhook is not a trusted state change.
Classify: Missing A1 + Missing C2. Ledger rows B* are N/A (P0).
Severity: Critical — double-charge and untrusted "paid" are in-scope.
Finding: A1+C2 | checkout + webhook routes | Critical | add intent-scoped
idempotency + verify-then-process. Do not demonstrate a replay or spoof.
Phase 0 — Detect payment surfaces & scope (gates every later finding) [LOW freedom — run exactly]
Find the money paths and the provider first. Never report an in-house-ledger
control as "Missing" on a pure merchant-integrator (N/A with a reason).
# Provider / SDK
rg -n --hidden -g '!node_modules' -i "stripe|paypal|braintree|adyen|square|payjp|paypay|razorpay|checkout\.com|worldpay|mollie|@stripe/|payment_intent|paymentintent" -l
# Money-movement verbs
rg -n -i "\b(charge|capture|authoriz|refund|void|payout|settle|chargeback|dispute|reversal)\b" -l
# Webhook endpoints + signature
rg -n -i "webhook|/webhooks?|constructEvent|verifyHeader|Stripe-Signature|x-signature|hmac" -l
# Idempotency
rg -n -i "idempotenc|idempotency[-_]?key|Idempotency-Key" -l
# Ledger / accounting
rg -n -i "ledger|double[-_ ]entry|debit|credit|journal|balance|posting|book(keeping)?" -l
# Reconciliation / settlement
rg -n -i "reconcil|settlement|settle|payout report|balance_transaction|three[-_ ]way" -l
# Money type (float smell = red flag)
rg -n -i "amount|price|money|currency|minor[-_ ]unit|cents" -g '*.{ts,tsx,js,py,go,java,rb,cs,sql}' -l
# Fraud / risk / SCA
rg -n -i "fraud|risk|velocity|3ds|3-?d ?secure|sca|radar|device.?fingerprint" -l
# Card-data smell (should find NOTHING raw)
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) [HIGH freedom]
Follow /research. Anchor to the installed SDK version and the provider's
current API (e.g. Stripe PaymentIntents, not the legacy Charges API).
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.
- Exact API params the integration should be sending —
stripe_api_search
then stripe_api_details on the operation id (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. Never
invent a param or endpoint the provider doesn't expose.
Phase 2 — Payment correctness matrix [HIGH freedom]
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.
- No PoCs — do not write replay, spoof, or card-testing procedures.
Phase 3 — Prioritized report (read-only) [HIGH freedom]
## 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 | Untrusted "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;
writing exploit or payment-fraud PoCs; editing payment code — this skill reports;
remediation is human-reviewed (route to a stronger model per the composer execution rule).
Self-critique before reporting [LOW freedom — do not skip]
- Evidenced —
file:line or "searched, none found", not "webhooks are probably signed"
- No PoC — finding names the missing control; no replay, spoof, or card-testing steps
- Tier respected — P2 ledger / 3-way match are N/A on P0, with why
- Severity justified — double-charge, lost money, or PAN/CVV = Critical
- Right owner — IAP / StoreKit →
audit-monetization-iap; per-call retry → audit-resilience
- Nothing edited — STOP-and-confirm; human-reviewed remediation only
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
audit-monetization-iap — StoreKit / Play Billing / RevenueCat (not Stripe/web)
complete-everything — close audited gaps to done with verification (human-reviewed for payment code)
1---2name: audit-payment-system3description: Read-only audit for payment/money-movement systems, scope-gated so a Stripe-Checkout site and an in-house ledger each see only relevant findings. Use when "audit payment system", "double charge / idempotency", "ledger / reconciliation", "webhook / 3DS / PCI". Mobile IAP → audit-monetization-iap.4license: MIT5---67# audit-payment-system — Money-Movement Correctness & Compliance Audit89**Degree of freedom: MIXED** — Scope, matrix, and severity `[HIGH freedom]`;10Phase 0 detection searches `[LOW freedom — run exactly]`. **Do not write11exploit or payment-fraud PoCs** — quote the missing control, never a replay12or spoof recipe.1314Read-only. Assess and prioritize; do not change code. Payment code is a15STOP-and-confirm surface — findings feed a human-reviewed remediation, ideally16with a stronger model. Delegations: per-call resilience → **`audit-resilience`**;17PCI/secrets/authz → **`audit-security`**; ledger schema → **`audit-db-schema`**;18outbox/saga *structure* → **`audit-backend-architecture`**; append-only19integrity → **`plan-data-integrity`**; Stripe integration → the Stripe plugin20skills. This skill owns *payment-domain correctness*.2122Payment failures are silent: a retried charge is a double-charge, a lost ledger23write is vanished money, a logged PAN is PCI liability, an unverified webhook24is an untrusted "paid". Three pillars: **idempotency**, a **double-entry25ledger** (when in scope), **reconciliation** — plus **PCI DSS v4.0.1** and26**webhooks as source of truth** (never trust the sync API response alone).2728---2930## Core principle — earn each control by scope; every gap is money or liability3132Not every app needs an in-house double-entry ledger. Stripe Checkout offloads33ledger, settlement, and most PCI — flagging "no double-entry ledger" there is34noise. **Idempotency, webhook verification, state sync, and tokens-only** apply35to *everyone* who moves money. Gate depth by scope (Phase 0). **Critical = a36customer is charged twice, money is lost/unaccounted, or card data is37exposed.** There is no "low severity" for a double-charge.3839## How to reason — Observe → Interpret → Classify → Severity40411. **Observe** — quote the mutation/webhook/ledger `file:line` (or "searched, none found")422. **Interpret** — what money or liability path breaks if that control is missing?433. **Classify** — Implemented / Partial / Missing / N/A (tier reason); control id (A1–G4)444. **Severity** — double-charge, lost money, or PAN/CVV exposure = Critical; P2-only rows are N/A on P04546## Worked example4748> **Observe:** P0 Stripe Checkout. `POST /api/checkout` calls49> `paymentIntents.create` with no idempotency key and no unique business-intent50> constraint (`app/api/checkout/route.ts`). Webhook handler updates order status51> from the parsed body without `constructEvent` / signature verification52> (`app/api/stripe/route.ts`).53> **Interpret:** a network retry can create two PaymentIntents for one checkout;54> an unverified webhook is not a trusted state change.55> **Classify:** Missing A1 + Missing C2. Ledger rows B* are N/A (P0).56> **Severity:** Critical — double-charge and untrusted "paid" are in-scope.57> **Finding:** A1+C2 | checkout + webhook routes | Critical | add intent-scoped58> idempotency + verify-then-process. Do not demonstrate a replay or spoof.5960---6162## Phase 0 — Detect payment surfaces & scope (gates every later finding) [LOW freedom — run exactly]6364Find the money paths and the provider first. Never report an in-house-ledger65control as "Missing" on a pure merchant-integrator (`N/A` with a reason).6667```bash68# Provider / SDK69rg -n --hidden -g '!node_modules' -i "stripe|paypal|braintree|adyen|square|payjp|paypay|razorpay|checkout\.com|worldpay|mollie|@stripe/|payment_intent|paymentintent" -l70# Money-movement verbs71rg -n -i "\b(charge|capture|authoriz|refund|void|payout|settle|chargeback|dispute|reversal)\b" -l72# Webhook endpoints + signature73rg -n -i "webhook|/webhooks?|constructEvent|verifyHeader|Stripe-Signature|x-signature|hmac" -l74# Idempotency75rg -n -i "idempotenc|idempotency[-_]?key|Idempotency-Key" -l76# Ledger / accounting77rg -n -i "ledger|double[-_ ]entry|debit|credit|journal|balance|posting|book(keeping)?" -l78# Reconciliation / settlement79rg -n -i "reconcil|settlement|settle|payout report|balance_transaction|three[-_ ]way" -l80# Money type (float smell = red flag)81rg -n -i "amount|price|money|currency|minor[-_ ]unit|cents" -g '*.{ts,tsx,js,py,go,java,rb,cs,sql}' -l82# Fraud / risk / SCA83rg -n -i "fraud|risk|velocity|3ds|3-?d ?secure|sca|radar|device.?fingerprint" -l84# Card-data smell (should find NOTHING raw)85rg -n -i "card[-_ ]?number|\bpan\b|cvv|cvc|card\.number|primary_account" -l86```8788Record a **payment profile** and pick the tier — apply only in-scope rows:8990| Tier | Signals | In scope |91|---|---|---|92| **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 |93| **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 |94| **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 |9596If there is **no** money movement (no PSP, no charge/ledger paths), stop and report reduced97applicability. If card data appears in the last `rg` above, that is **Critical, report immediately**.9899---100101## Phase 1 — Research (version-anchored, provider-aware) [HIGH freedom]102103Follow `/research`. Anchor to the **installed** SDK version and the provider's104*current* API (e.g. Stripe **PaymentIntents**, not the legacy Charges API).105Confirm the current-year shape of the controls before judging the code.106107**When the provider is Stripe, use the Stripe MCP as the authoritative source:**108109- Concepts / best practice (idempotency keys, webhook signature verification,110 PaymentIntents lifecycle, SCA/3DS2, Radar) — `search_stripe_documentation`111 with `search_only_api_ref: false`.112- Exact API params the integration should be sending — `stripe_api_search`113 then `stripe_api_details` on the operation id (confirm `PaymentIntent.create`114 is called with an idempotency key and amounts in minor units).115116For **PayPal / Square / Adyen / PayPay / Braintree / others**, the Stripe MCP117does not apply — use `/research` against the provider's official docs. Never118invent a param or endpoint the provider doesn't expose.119120---121122## Phase 2 — Payment correctness matrix [HIGH freedom]123124For **each in-scope row**, mark `Implemented / Partial / Missing / N/A` with125`file:line`, a one-line "why it bites in prod", and the fix-delegate. **Full126detection commands, good-vs-red-flag signals, and fix targets are in127[references/checklist.md](references/checklist.md)** — load it and work the128applicable groups.129130### A. Money-movement correctness (P0+)131| # | Control | Bites in prod if missing | Fix via |132|---|---|---|---|133| 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` |134| 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` |135| 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` |136| 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` |137| 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` |138139### B. Ledger & data integrity (P1 internal balances · P2 full ledger)140| # | Control | Bites in prod if missing | Fix via |141|---|---|---|---|142| 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` |143| 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` |144| 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` |145| 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` |146| 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` |147148### C. Async orchestration & webhook delivery (P0+)149| # | Control | Bites in prod if missing | Fix via |150|---|---|---|---|151| 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` |152| C2 | **Webhook signature verified** (HMAC / provider `constructEvent`) before any processing | Spoofed "payment succeeded" → goods shipped for free | `audit-security` |153| 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` |154| 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` |155| 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` |156| 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` |157158### D. Reconciliation & settlement (P1/P2)159| # | Control | Bites in prod if missing | Fix via |160|---|---|---|---|161| 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` |162| 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` |163| 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` |164| D4 | **Safety brake** — unreconciled balance over a threshold halts new captures / alerts | Losses accumulate faster than they're caught | `audit-resilience` |165166### E. Fraud, risk & SCA (P0 delegate · P1/P2 in-house)167| # | Control | Bites in prod if missing | Fix via |168|---|---|---|---|169| 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` |170| 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` |171| 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` |172| E4 | **Chargeback / dispute monitoring** — track ratio, react before acquirer watchlist (VAMP/VDMP) thresholds | Program placement / fines; account termination | `backend-observability` |173| E5 | **AML / sanctions screening** (P2 / regulated) | Regulatory exposure for regulated flows | `/research` + human |174175### F. Compliance & security — PCI DSS v4.0.1 (all tiers)176| # | Control | Bites in prod if missing | Fix via |177|---|---|---|---|178| 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` |179| F2 | **Tokenization** — hosted fields / PaymentIntents so raw card data bypasses your infra | Balloons PCI scope from SAQ-A to full audit | provider docs |180| 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` |181| 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` |182183### G. Error handling & resilience (P0+)184| # | Control | Bites in prod if missing | Fix via |185|---|---|---|---|186| G1 | **PSP/bank API timeout + retry with backoff** and a **circuit breaker** | One slow provider exhausts the pool → whole checkout 503s | `audit-resilience` |187| G2 | **Bulkhead / pool isolation** — PSP calls can't starve the DB/other deps | Timeout storm cascades across the system | `audit-backend-architecture` |188| G3 | **Partial-write safety** — state + ledger commit atomically; no "charged but not booked" | Money taken, ledger never posted (or reverse) | `backend-patterns` |189| G4 | **Graceful degradation** for non-critical deps (fraud/notification down ≠ block auth, per policy) | A non-critical outage takes payments offline | `audit-resilience` |190191Rules:192- **Evidence or it didn't happen** — every verdict cites `file:line` or "searched, none found".193- **N/A is first-class** — record *why* (scope tier), don't drop the row.194- **No double-counting** — link per-call resilience to `audit-resilience`, PCI to `audit-security`.195- **No PoCs** — do not write replay, spoof, or card-testing procedures.196197---198199## Phase 3 — Prioritized report (read-only) [HIGH freedom]200201```markdown202## Payment System Audit — [repo] — [date]203**Provider(s):** [Stripe/PayPal/…] · **Scope tier:** [P0/P1/P2 + evidence]204**In scope:** [groups] · **N/A (out of tier):** [rows + why]205206### Critical — money loss / double-charge / card-data exposure (fix before ship)207| Finding | Control | file:line | Why it bites | Fix via |208|---|---|---|---|---|209| Charge has no idempotency key; no unique constraint | A1 | pay/charge.ts:52 | Retry double-charges the customer | audit-resilience |210| Webhook processed without signature check | C2 | api/webhook.ts:9 | Untrusted "paid" → free goods | audit-security |211| Card number written to app log | F1 | pay/log.ts:20 | PCI breach liability | audit-security |212| DB write then broker publish (not atomic) | C4 | ledger.ts:88 | Captured, never booked → money unaccounted | backend-patterns |213214### High / Medium (correctness & compliance matrix)215| Group | Implemented | Partial | Missing | N/A | Fix via |216|---|---|---|---|---|---|217| A Money-movement | … | … | … | | audit-resilience |218| B Ledger | … | … | … | (P0) | audit-db-schema |219| C Webhooks/async | … | … | … | | backend-patterns |220| D Reconciliation | … | … | … | (P0) | data-pipeline |221| E Fraud/SCA | … | … | … | | backend-patterns |222| F PCI/compliance | … | … | … | | audit-security |223| G Resilience | … | … | … | | audit-resilience |224225### Lift-to-production roadmap (ordered by blast radius)2261. Idempotency (gateway + DB unique constraint) on every mutation → audit-resilience2272. Verify + dedup webhooks, 200-then-process async → audit-security / audit-resilience2283. Atomic state+ledger+outbox; pull-based recovery for stuck payments → backend-patterns2294. [P1/P2] Double-entry append-only ledger + daily reconciliation w/ break report → audit-db-schema / data-pipeline2305. Tokens-only + key rotation + access audit (PCI v4.0.1) → audit-security2316. Breaker/bulkhead + fraud fail-policy around every external call → audit-resilience232```233234**Forbidden:** declaring "production-grade" from passing tests alone; flagging P2-only controls235(in-house ledger, 3-way bank match, sharding) against a P0 merchant integrator; re-auditing per-call236timeouts/retries `audit-resilience` owns; assigning any severity below Critical to a double-charge,237lost-money, or PAN-exposure finding; recommending a refund/payout saga without compensation logic;238**writing exploit or payment-fraud PoCs**; **editing payment code** — this skill reports;239remediation is human-reviewed (route to a stronger model per the composer execution rule).240241---242243## Self-critique before reporting [LOW freedom — do not skip]2442451. **Evidenced** — `file:line` or "searched, none found", not "webhooks are probably signed"2462. **No PoC** — finding names the missing control; no replay, spoof, or card-testing steps2473. **Tier respected** — P2 ledger / 3-way match are N/A on P0, with why2484. **Severity justified** — double-charge, lost money, or PAN/CVV = Critical2495. **Right owner** — IAP / StoreKit → `audit-monetization-iap`; per-call retry → `audit-resilience`2506. **Nothing edited** — STOP-and-confirm; human-reviewed remediation only251252## Related253- `audit-resilience` — per-call idempotency keys, timeouts, retry+backoff+jitter, circuit breaker, cancellation254- `audit-security` — PCI/PAN handling, webhook auth, secrets, key rotation, injection, access logging255- `audit-db-schema` — double-entry ledger schema, append-only constraints, money types, partitioning256- `audit-backend-architecture` — outbox/saga/breaker *structure* and topology fit (this skill checks payment *correctness* on top)257- `plan-data-integrity` — append-only/immutability guarantees and destructive-op safety258- `plan-secrets-audit` — rotate vs relocate provider API/signing keys259- `backend-patterns` / `backend-patterns/references/architecture-patterns.md` — implement idempotency, outbox, saga, state machine260- `data-pipeline` — reconciliation/settlement ingestion jobs261- the Stripe plugin skills (`stripe-best-practices`, `connect-recommend`, `upgrade-stripe`) — Stripe-specific integration262- `audit-monetization-iap` — StoreKit / Play Billing / RevenueCat (not Stripe/web)263- `complete-everything` — close audited gaps to done with verification (human-reviewed for payment code)