The Paddle credit flow credited an amount taken from client transaction metadata, never tied to what was actually paid. Move the decision server-side: the API records a credit_purchase_intents row (server-computed amount, keyed by transaction id) at creation; the webhook and callback credit that amount and refuse when no row exists. Metadata is display-only now.
Also adds UNIQUE on credit_ledger.paddle_transaction_id + ON CONFLICT DO NOTHING on every grant path, replacing a racy SELECT-then-INSERT.
Two unrelated bugs found while testing, fixed in their own commits: metering's get_user_usage query (syntax + GROUP BY + NUMERIC-as-f64), and a stale usage_records ref in the e2e test (now usage_ledger).
Reviewer:
migration 044 adds the table + constraint; the UNIQUE fails to apply if the DB holds duplicate paddle_transaction_id rows from the old race — run the dupe-check first. In-flight pre-deploy transactions have no intent row and won't auto-credit (reconcile manually).
What I tested already:
make test-e2e-billing against a real stack — 37/37, including four new cases: credits the intent amount and ignores a forged custom_data value, refuses with no intent row, credits once on duplicate delivery, and the UNIQUE/ON CONFLICT no-op.