feat(credits): charge credits per chat turn (atomic wallet debit + audit)

Merged
SW
5mo ago
+533-79

Summary

Closes the silent revenue-loss gap tracked in #605 — every successful chat workflow turn on `/api/chat/workflow` now debits the account's wallet and records a `usage_events` audit row, atomically.

Built on the `deduct_credits_with_audit` Postgres function shipped in recoupable/database#26.

End-to-end flow

  1. `runAgentStep` already captures `responseMessage.metadata` (`totalMessageCost`, `totalMessageUsage`) via `onFinish` — same number the chat UI shows next to the assistant response.
  2. `runAgentWorkflow` now calls `recordChatUsage(accountId, modelId, responseMessage)` after `persistAssistantMessage`.
  3. `recordChatUsage` → `computeCreditsDeductedCents` (gateway cost → token estimate → 1c floor) → `deductCreditsWithAudit`.
  4. `deduct_credits_with_audit` runs the wallet `UPDATE` + audit `INSERT` in one PL/pgSQL transaction. Either both land or neither does — matches open-agents' `db.transaction(...)` atomicity guarantee.

`accountId` is threaded through `RunAgentWorkflowInput` from `validateChatWorkflow` (auth-derived — never trusted from the request body).

New files (all TDD: red → minimal impl → green)

FilePurposeTests
`lib/credits/AvailableModelCost.ts`Cost-catalog typetype-only
`lib/credits/estimateModelUsageCost.ts`Token-based USD estimator with 200k+ tier swap13
`lib/credits/computeCreditsDeductedCents.ts`Gateway cost → estimate → 1c floor14
`lib/supabase/credits_usage/deductCreditsWithAudit.ts`Thin `supabase.rpc` wrapper, fire-and-forget contract4
`app/lib/workflows/recordChatUsage.ts``"use step"` that ties it all together8

Updates

  • `app/lib/workflows/runAgentWorkflow.ts` — `accountId` field on `RunAgentWorkflowInput` + `recordChatUsage` call after `persistAssistantMessage`
  • `lib/chat/handleChatWorkflowStream.ts` — passes `validated.accountId` into `start(runAgentWorkflow, ...)`
  • `app/lib/workflows/tests/runAgentWorkflow.test.ts` — 3 new tests (records on success, skips when no responseMessage, skips when runAgentStep throws)

Total: 42 new unit tests. Suite: 3205 → 3220 passing.

Test plan

  • 42 new unit tests pass
  • Full suite passes (3220/3220)
  • Lint / format clean
  • Manual preview verification — trigger a chat workflow on the preview and confirm:
    1. `GET /api/accounts/{accountId}/credits` shows `remaining_credits` decrement
    2. `/api/admins/credits/events` shows a fresh `usage_events` row with `source='api'`, `agent_type='main'`, the model id, token counts, and `credits_deducted_cents` matching the chat UI's displayed cost
    3. Runtime logs show `[recordChatUsage] success { accountId, modelId, cents }`

Why one PR instead of "step 2/6"

The previous slice (#611) was lib code only with no integration point — preview deployments couldn't exercise the new functions. Folded back together so this PR ships a complete user-observable change.

🤖 Generated with Claude Code


Summary by cubic

Charges credits for each successful chat turn on /api/chat/workflow, debiting the wallet and writing a usage_events audit row atomically. Billing is consolidated into handleChatCredits and prefers gateway-reported cost to match the UI, closing the free-turn gap tracked in #605.

  • Refactors

    • Integrated handleChatCredits at the end of runAgentWorkflow after persistAssistantMessage; threaded accountId from validateChatWorkflow through handleChatWorkflowStream and RunAgentWorkflowInput.
    • getCreditUsage accepts gatewayCostUsd and short-circuits when positive; otherwise falls back to token pricing with a 1c floor.
    • recordCreditDeduction now calls deduct_credits_with_audit via lib/supabase/credits_usage/deductCreditsWithAudit.ts for atomic debit + audit; adds source labeling (api for workflow, default web elsewhere).
    • Workflow cleanup: runAgentWorkflow reads AgentMessageMetadata and uses a ZERO_USAGE fallback when metadata is missing (no behavior change).
  • Bug Fixes

    • Flattened DeductCreditsWithAuditResult to avoid Next.js 16 narrowing issues.
    • Regenerated types/database.types.ts to include deduct_credits_with_audit and cast params.event to Json in the RPC wrapper.
    • Marked recordCreditDeduction with 'use step' so nanoid runs in the workflow step runtime.

Summary by CodeRabbit

  • New Features

    • Added API source labeling to distinguish usage charges between API and web interface activity.
    • Introduced support for gateway-reported per-turn costs in credit calculations.
  • Bug Fixes

    • Improved billing reliability by implementing atomic transaction processing for credit deductions and audit logging, ensuring consistency between wallet updates and usage records.

Review Change Stack