From 4c5f0584aa2924714ffcd2915f08d12dc0092978 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 25 May 2026 14:02:46 -0500 Subject: [PATCH] feat: add deduct_credits_with_audit RPC for atomic wallet+meter writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new PL/pgSQL function that debits credits_usage.remaining_credits and inserts the corresponding usage_events audit row inside a single transaction. Mirrors open-agents' recordUsage pattern (apps/web/lib/db/usage.ts) which uses db.transaction(...) for the same guarantee — the cubic code-review concern about wallet drifting past the meter when one side fails. The existing deduct_credits(account_id, amount) only updates the wallet, and a separate INSERT INTO usage_events from PostgREST is not atomic with it. This function consolidates both writes so chat-workflow and research callers can debit + audit in one round trip. No schema changes — usage_events was already widened in 20260515000000_usage_events_unify_with_credits.sql to absorb chat callers (source='api', credits_deducted_cents column). This is purely a new function. Unblocks the chat-workflow credits gap tracked in recoupable/api#605 (silent revenue loss — every chat turn is currently free on api). Co-Authored-By: Claude Opus 4.7 (1M context) --- ...260525000000_deduct_credits_with_audit.sql | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 supabase/migrations/20260525000000_deduct_credits_with_audit.sql diff --git a/supabase/migrations/20260525000000_deduct_credits_with_audit.sql b/supabase/migrations/20260525000000_deduct_credits_with_audit.sql new file mode 100644 index 0000000..41d23c9 --- /dev/null +++ b/supabase/migrations/20260525000000_deduct_credits_with_audit.sql @@ -0,0 +1,77 @@ +-- Atomic credit debit with audit trail. +-- +-- Mirrors open-agents' recordUsage pattern +-- (apps/web/lib/db/usage.ts:60) which wraps wallet debit + meter +-- insert in a single transaction so the two never drift apart on +-- failure (the cubic code-review concern). +-- +-- PostgREST cannot execute multi-statement transactions, so the +-- atomic guarantee has to live in a PL/pgSQL function — function +-- bodies run inside an implicit transaction. Either both writes +-- commit or both roll back together. +-- +-- Args: +-- p_account_id account whose wallet is being debited +-- p_amount debit amount in cents (matches the unit on +-- usage_events.credits_deducted_cents and the cents +-- produced by open-agents' computeCreditsDeductedCents) +-- p_event_id caller-supplied audit row id (matches the nanoid +-- convention in lib/supabase/usage_events/insertUsageEvent.ts; +-- explicit so callers can correlate with workflow runs) +-- p_event JSON payload for the usage_events row: +-- { source, agent_type, provider, model_id, +-- input_tokens, cached_input_tokens, output_tokens, +-- tool_call_count } +-- All fields optional; absent fields fall back to +-- column defaults (source='api', agent_type='main', +-- token/tool counts=0, provider/model_id NULL). +-- +-- Replaces the per-caller pattern of `deduct_credits(...)` followed +-- by a separate `INSERT INTO usage_events` — those two writes are +-- NOT atomic through PostgREST and can drift on partial failure. + +CREATE OR REPLACE FUNCTION public.deduct_credits_with_audit( + p_account_id uuid, + p_amount integer, + p_event_id text, + p_event jsonb +) RETURNS void + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public, pg_temp +AS $$ +BEGIN + UPDATE public.credits_usage + SET remaining_credits = remaining_credits - p_amount + WHERE account_id = p_account_id; + + INSERT INTO public.usage_events ( + id, + account_id, + source, + agent_type, + provider, + model_id, + input_tokens, + cached_input_tokens, + output_tokens, + tool_call_count, + credits_deducted_cents + ) VALUES ( + p_event_id, + p_account_id, + coalesce(p_event->>'source', 'api'), + coalesce(p_event->>'agent_type', 'main'), + p_event->>'provider', + p_event->>'model_id', + coalesce((p_event->>'input_tokens')::int, 0), + coalesce((p_event->>'cached_input_tokens')::int, 0), + coalesce((p_event->>'output_tokens')::int, 0), + coalesce((p_event->>'tool_call_count')::int, 0), + p_amount + ); +END; +$$; + +GRANT EXECUTE ON FUNCTION public.deduct_credits_with_audit(uuid, integer, text, jsonb) + TO authenticated, service_role;