Skip to main content
AC
Quality & Security5.5 KBMIT licensed

money-path-halt-go

Original, written for TechTide client work

Mandatory HALT-GO review for any diff touching money surfaces, checkout, payment intents, webhooks, payouts, refunds, ledgers, balances, orders, or invoices, on a Stripe/Paddle + Supabase stack. Use whenever a change touches those code paths or tables, however small. Blocks ship until every gate passes. Not for general schema rules (use supabase-schema-discipline) or the broader pre-ship security sweep (use rls-security-gate).

  • money
  • path
  • halt

SKILL.md

Money-Path HALT-GO

Money bugs do not degrade gracefully, they leak. Treat any in-scope change as production-critical no matter how small the diff looks.

Trigger surfaces

Invoke this skill if the diff touches any of:

  • Stripe or Paddle SDK calls, webhook handlers, checkout session creation, payment intents, subscriptions, refunds.
  • Payout, transfer, or Connect logic.
  • Tables matching *balance*, *ledger*, *wallet*, *order*, *invoice*, *payment*, *payout*, *transfer*, *escrow*, *hold*, *settlement*, *trade*, *credit*, *charge*.
  • Pricing tables or amount fields (amount, price, fee, total_cents).
  • Any code that writes a monetary value derived from user input.

HALT: state the change in writing first

Before any edit, produce:

WHAT CHANGES: <one paragraph>
MONEY EFFECT: <who pays or receives how much, in what currency, when>
TRUST BOUNDARY: <where untrusted input enters>
REVERSIBILITY: <can it be undone, and how>
BLAST RADIUS: <users, rows, dollars affected if wrong>

Cannot fill all five? Stop and ask the user.

GO: every gate must pass

  1. Amounts are server-derived. The client never sends the final charge amount. The server recomputes from product ID, quantity, and persisted price. Currency is server-pinned per product. Coupons validate server-side.
  2. Idempotency. Every money-moving write persists an idempotency key in a UNIQUE-indexed column. Provider calls pass an Idempotency-Key header derived from it. Re-processing the same webhook event is a no-op, deduped on event.id.
  3. Webhooks fail closed. Signature verified with timingSafeEqual or the provider SDK before any DB write. Raw body read before JSON parse. Invalid signature returns 401 with no body logged. Event persisted to a webhook_events table before side effects, and side effects key to that row.
  4. Ledger discipline (if internal balances exist). Money moves as double-entry rows, debit plus credit in one transaction. No UPDATE balance SET amount = amount + X without a ledger row in the same transaction. Balances are derivable from the ledger, never the sole source of truth. Money columns are integer cents or numeric(20,4), never float.
  5. RLS on money tables. Users SELECT only their own rows. No TO anon policy. No broad UPDATE or DELETE. Writes go through server functions, never direct from the browser. Admin-only tables gated by a role check.
  6. Destructive op guardrails. No migration drops or renames a money column without explicit user confirmation in chat. No backfill UPDATE on a money table without a dry-run row count first.
  7. Test the failure mode, not the happy path. State and verify at minimum: webhook arrives twice, user closes the tab mid-checkout, provider returns 5xx after charging, RLS denies the post-payment write.

Good vs bad

Good: server reads price_cents from the products table, computes the total, creates the payment intent with an idempotency key, records the charge as two ledger rows in one transaction.

Bad: createCheckout({ amount: req.body.total }). The client picked the price. Anyone with devtools pays one cent. Gate 1 exists because this exact bug ships constantly.

Verification

Do this: walk gates 1 through 7 against the diff and write one line of evidence per gate (file and line, or query output). Expect seven PASS lines. If any gate lacks evidence or fails, state which, propose the fix, and do not ship.

Record the result:

money-path HALT-GO: PASS
- Amounts: server-derived from <source>
- Idempotency: <key strategy>
- Webhook: <signature verification location>
- Ledger: <yes/no, double-entry confirmed>
- Failure modes verified: <list>

Completion checklist

  • [ ] HALT block written before the first edit
  • [ ] All seven gates pass with evidence
  • [ ] Failure modes exercised, not just described
  • [ ] PASS record appended to the project log
  • [ ] rls-security-gate run after this skill

Any box unchecked: not done. Fix or say so.

Footguns

  • Webhook handler parses JSON before verifying the signature: verification then runs on a re-serialized body and fails intermittently. Read the raw body first, always.
  • Idempotency key generated per-request (UUID at call time): retries create duplicate charges. Derive the key deterministically from stable inputs like ${user_id}:${order_id}:${action}.
  • Float money math: 0.1 + 0.2 bugs land in real balances. Integer cents only.
  • "Ack first, process later" webhook pattern without persisted dedup: a redelivery double-applies the side effect.

Red Flags: stop if you think or read any of these

  • "It's a one-line change to the checkout"
  • "The webhook is internal, nobody else knows the URL"
  • "We can add idempotency later"
  • "The client already validates the price"
  • "It worked in the test dashboard"
  • "This table isn't really money, it's just credits"

Every one of these means: run the full HALT-GO. There is no diff size, table name, or deadline that exempts a money path from these gates.

More in Quality & Security

All skills