--- title: "PSP Webhook Double Credit" canonical: "https://mumo.chat/p/psp-webhook-double-credit-m7m39h" machine_version: 1 models: ["GPT","Grok","Muse"] round_count: 3 published_at: "2026-07-11T17:07:23.156109+00:00" updated_at: "2026-07-11T17:12:43.038+00:00" brief_source: "session_takeaway" full: "https://mumo.chat/p/psp-webhook-double-credit-m7m39h.md" --- > Brief of a mumo multi-model deliberation — the session synthesis > without the transcript. This is an orientation surface; to audit the > synthesis against the models' actual prose, read the full transcript. > Full deliberation with evidence: https://mumo.chat/p/psp-webhook-double-credit-m7m39h.md # PSP Webhook Double Credit *A multi-model AI conversation on mumo* *July 11, 2026* **Panel:** GPT · Muse · Grok · 3 rounds --- ## Question Pressure-test a proposed fix for a PSP webhook double-credit bug in our B2B card-issuing and wallet platform, and tell me what else I might be missing before we ship. ## System Node webhook service + Postgres. Money path tables: - `processed_psp_events(event_id TEXT PRIMARY KEY, event_type, received_at, processed_at)` - `wallet_ledger_entries(id, wallet_id, psp_event_id, payment_intent_id, direction, amount_cents, currency, entry_type, created_at)` - intended invariant: one PSP economic fact → at most one wallet credit for that fact We also have ~$340k in customer wallet top-ups in flight today. This ticket is P0 for a launch customer (wallet `wal_8a4f9`). ## Bug (consistent algorithm story) Customer reported wallet balance jumped +$1,200 twice for one top-up. The PSP dashboard shows a single `payment_intent.succeeded` / `charge.succeeded` chain for `pi_3M…`. In Postgres we found two `wallet_ledger_entries` credits for the same `payment_intent_id`, and only one `processed_psp_events` row for `evt_1Qp…`. Handler algorithm that ran in prod (not the algorithm we wished we had): 1. `SELECT 1 FROM processed_psp_events WHERE event_id = $1` 2. If missing → `creditWallet(...)`… *(prompt truncated — full text in the full transcript)* ## Session Takeaway *(mumo-generated synthesis of the whole session — evidence lives in the full transcript)* **You cannot safely patch this live; you must fully quiesce the system, mark canonical records, and only then cut over to a constraint-enforced ledger.** The moderator opened by pressure-testing a proposed fix for a PSP webhook double-credit bug, specifically asking about advisory locks, idempotency keys, and deployment risks. The panel converged on fact-owned uniqueness and proof-before-200, then debated whether to keep advisory locks or rely solely on database constraints. The session closed on a hard pivot: the proposed 'live patch' strategy is unsafe because historical duplicates block the necessary indexes, forcing a full system quiescence to repair data and enforce constraints before reopening traffic. ### Arcs #### SHIFTED — Drop the advisory lock; the unique index handles serialization. (Rounds 1, 2) The panel initially debated keeping a fact-keyed advisory lock to manage contention, but ultimately agreed it was redundant and risky. The unique index on the economic fact already serializes concurrent inserts, whereas a lock with a timeout would convert harmless contention into 5xx errors, amplifying the PSP retry storm rather than draining it. #### EMERGED — Stop all traffic before fixing; don't rely on database constraints alone. (Rounds 2, 3) A critical collision emerged in the final round: the index needed to protect against old code cannot be built because historical duplicates block it, while the index that can build does not constrain old code. This structural deadlock means you cannot safely patch live; a total stop of intake is mandatory to drain workers, apply repairs, and ensure no overlap between old and new logic. #### EMERGED — Use a status marker to isolate historical duplicates from new credits. (Round 3) To resolve the index-building deadlock, the panel converged on adding a status column (like voided_at) to mark historical duplicates. This allows the unique index to ignore old mess while still forcing legacy code—which defaults to NULL or canonical—to fail-closed if it tries to double-credit, preserving financial history without blocking enforcement. --- ## Round Map - **Round 1:** The proposed fix works for preventing future double-credits, provided you lock on the economic fact rather than the event ID and stop treating the inbox row as a sufficient proof of completion. - **Round 2:** Drop the advisory lock because your unique index already handles serialization, and use a strictly sequenced deployment that builds enforcement constraints before cutting over to ensure the old code fails safe rather than double-crediting. - **Round 3:** You cannot safely patch this live; you must fully quiesce the system, mark canonical records, and only then cut over to a constraint-enforced ledger. --- **Full deliberation with evidence:** https://mumo.chat/p/psp-webhook-double-credit-m7m39h.md