# Set the credits aside before admitting paid work

*When a paid job is interrupted · SQL + concurrency test report. From the portfolio of Guilherme Rohden. Excerpts are quoted verbatim; internal paths and identifiers are redacted.*

The database lock shared by reserving and spending credits, the capture path that rolls back on a failed charge, and the three-way concurrency test.

## The decision

Several jobs can each read a sufficient balance and start against the same credits (HYVIRAL’s prepaid units), and a rejected charge cannot undo provider costs already incurred. So billing sets the credits aside before paid work and settles the actual amount afterwards.

A hold reduces the amount available to other jobs without immediately charging the wallet. Placement shares a transaction-scoped wallet lock with credit consumption. An active reference is reusable, so retrying placement does not create a second reservation.

The cost is lifecycle management: held credits are temporarily unavailable, and unsuccessful or abandoned work needs release or expiry handling.

**The lifecycle:** place an active hold → run the admitted paid work → capture the actual amount, or release or expire the hold. Repeating a completed capture returns its first result; a failed consumption leaves the transaction uncommitted.

## From the source

**Placement SQL.** One lock key serializes placement and consumption for the same wallet:

```sql
  -- Same lock key as consume_credits_with_priority: placements and consumes for
  -- one user serialize against each other, which is what makes the check below
  -- authoritative rather than advisory.
  PERFORM pg_advisory_xact_lock(
    hashtextextended('billing.wallet:' || p_user_id::text, 0)
  );
```

**Capture SQL.** Failed consumption raises inside the transaction, rolling back the captured-state update:

```sql
    IF NOT v_consume.success THEN
      -- Surface rather than swallow: the hold guaranteed this could be paid, so
      -- a denial here means the guarantee was violated and the caller must see
      -- it. Raising rolls back the status update too, leaving the hold active
      -- for the reaper rather than stranding it as captured-but-unpaid.
      RAISE EXCEPTION 'capture_credit_hold: consume denied (%)',
        COALESCE(v_consume.error_message, 'unknown');
    END IF;
```

**Concurrency test, as reported in the 12 August 2026 specification:**

> - **The Problem Statement's scenario, reproduced and closed.** Available squeezed to exactly 3,000; three concurrent `place_credit_hold(2400)` calls. **Exactly one succeeded**; the other two were denied with `Insufficient available credits`.

## What this shows

The mechanism is in the SQL: a shared advisory lock makes the balance check authoritative, and capture cannot leave a hold marked captured without the matching charge. The reported test shows the admission behavior under contention: **one admission and two denials** for 2,400 credits each against 3,000 available.
