AI video API budget reservations: an architecture tutorial
Two video workers each see $0.20 available and each submit a $0.176 job. Both checks pass; together they commit $0.352. AI video API budget reservations prevent this race by allocating spending capacity before submission, rather than checking a balance and hoping concurrent workers leave it unchanged.
This tutorial designs an application-side budget ledger for GenRelay integrations. A reservation is a durable allocation of budget to one generation attempt. It is not a charge, a provider-side wallet hold, or a claimed GenRelay feature. The examples use local Python and SQLite, send no network requests, and require no API credentials.
Updated October 31, 2026. Prices below are verified user-facing rates as of this date; recheck applicable pricing before deployment.
What invariant should AI video API budget reservations enforce?
Enforce settled spend + active reservations <= budget limit whenever admitting new work.
An invariant is a condition that every relevant database transaction must preserve. Settled spend means recorded charges; active reservations represent exposure not yet resolved into a final charge. Available budget equals the limit minus both amounts. Use integer microdollars, where one dollar equals 1,000,000 units, to avoid floating-point rounding errors.
For this example, the budget is a fixed allocation for one project. Production systems should identify each account by tenant, project, currency, and budget period. Never erase an outstanding reservation at midnight: it still represents potential spend. Keep its originating period and apply an explicit attribution policy to subsequent settlement.
This design controls admission, not upstream billing. If a final charge exceeds the reserved estimate, record the full liability and freeze further admission when necessary. Do not truncate actual charges to make a dashboard appear compliant. Strict spending bounds require a validated upper bound for every accepted request and complete coverage of all submission paths.
How much should you reserve for a video attempt?
Reserve the validated maximum charge for the specific request, using its billing unit rather than a generic video price.
Use these verified rates as of October 31, 2026:
| Model or variant | Billing unit | Reservation calculation |
|---|---|---|
| Grok 1.5 | $0.022 per second | Billable seconds × $0.022 |
| Grok 1.0 | $0.010 per second | Billable seconds × $0.010 |
| Omni Flash 720P | $0.10 per generation | Generations × $0.10 |
| Omni Flash 1080P | $0.15 per generation | Generations × $0.15 |
An eight-second Grok 1.5 calculation is $0.176; eight seconds at the Grok 1.0 rate is $0.080. These are arithmetic examples, not assertions about supported duration settings. Validate permitted durations and billing rules against the selected model's current documentation. Omni Flash uses per-generation user pricing here: do not multiply its rate by duration.
Snapshot the price version, model, resolution, requested quantity, and billable-duration assumption with each reservation. If an option has an unknown price, reject or route it for review instead of assigning a zero estimate. If multiple candidates are generated, reserve for every potentially billable candidate.
Creative rejection does not establish that a generation is free. Track that distinction in your cost per approved clip analysis. For a different model family, consult the Veo model page and validate its current billing dimensions rather than reusing these rates.
How do you make admission atomic across workers?
Perform the duplicate check, capacity check, and reservation insert inside one database transaction.
Atomic means the transaction's changes commit together or do not commit. An idempotency key is an application identifier that makes repeated handling of the same logical attempt return the existing result. It prevents duplicate reservations locally; it does not establish that a provider deduplicates submissions.
This standalone Python example uses SQLite's BEGIN IMMEDIATE to serialize competing writers. Each worker opens its own connection. The database is created inside a temporary directory, and the assertions verify that competing reservations cannot exceed the $0.20 allocation.
import sqlite3
import tempfile
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
def reserve(path, attempt, amount):
if not isinstance(amount, int) or amount <= 0:
raise ValueError("amount must be positive integer microdollars")
db = sqlite3.connect(path, timeout=10)
try:
db.execute("BEGIN IMMEDIATE")
previous = db.execute(
"SELECT amount FROM holds WHERE attempt = ?", (attempt,)
).fetchone()
if previous:
if previous[0] != amount:
raise ValueError("attempt reused with a different amount")
db.commit()
return True
limit, spent = db.execute(
"SELECT cap, spent FROM account WHERE id = 1"
).fetchone()
held = db.execute(
"SELECT COALESCE(SUM(amount), 0) FROM holds"
).fetchone()[0]
if spent + held + amount > limit:
db.rollback()
return False
db.execute("INSERT INTO holds VALUES (?, ?)", (attempt, amount))
db.commit()
return True
except Exception:
db.rollback()
raise
finally:
db.close()
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "budget.sqlite"
db = sqlite3.connect(path)
db.executescript("""
CREATE TABLE account (
id INTEGER PRIMARY KEY, cap INTEGER, spent INTEGER
);
INSERT INTO account VALUES (1, 200000, 0);
CREATE TABLE holds (
attempt TEXT PRIMARY KEY, amount INTEGER NOT NULL
);
""")
db.close()
attempts = ["attempt-a", "attempt-b"]
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(
lambda key: reserve(path, key, 176000), attempts
))
assert sum(results) == 1
winner = attempts[results.index(True)]
assert reserve(path, winner, 176000)
db = sqlite3.connect(path)
assert db.execute("SELECT COUNT(*) FROM holds").fetchone()[0] == 1
assert db.execute("SELECT SUM(amount) FROM holds").fetchone()[0] == 176000
db.close()
print("Atomic admission assertions passed")
This is an admission demonstration, not a complete service. In production, bind the attempt key to an immutable request fingerprint—a digest of the relevant request parameters—not merely its amount. Two different requests can cost the same. Scope uniqueness to the tenant, and check every applicable project and organization limit in the same transaction with a consistent lock order.
What should happen between reservation and submission?
Commit the reservation with a durable dispatch record, then submit outside the budget transaction.
A transactional outbox is a database record of pending work written in the same transaction as its associated state change. It closes the crash window between allocating funds and publishing work to a queue. A dispatcher claims that record, records its submission attempt, and performs the network operation without holding database locks.
Use a state machine, meaning an explicit set of states and permitted transitions:
- Reserved: capacity allocated; dispatch pending.
- Submitting: a dispatcher has started the submission path.
- Submitted: a provider job identifier has been durably recorded.
- Uncertain: acceptance cannot yet be determined.
- Settled: authoritative billing evidence has resolved the exposure.
Do not interpret a connection timeout as rejection. The remote service may have accepted the request before the connection failed. Keep the reservation active, preserve correlation data, and investigate. A worker crash after acceptance but before recording the identifier creates the same uncertainty.
A claimed outbox record does not produce exactly-once remote execution. Resubmit automatically when documented provider idempotency or authoritative evidence makes that safe; otherwise pause for reconciliation. See AI video API job recovery for the surrounding recovery workflow. No endpoint or provider capability is assumed here.
How should settlement handle duplicate events and uncertain jobs?
Settlement should atomically replace a reservation with the authoritative charge, while uncertainty leaves the hold intact.
Reconciliation means comparing your local ledger with authoritative job and billing evidence. A completed video may still need billing reconciliation. Likewise, a failed or canceled job is not sufficient evidence of zero charge. Release capacity after confirmed nonacceptance or a definitive zero-charge outcome, not merely after an application timeout.
The second standalone example demonstrates local transitions. Its charge values are synthetic test fixtures, not a partial-charge policy. The caller is assumed to have validated billing evidence before invoking settle. Repeated identical settlement is harmless; conflicting settlement raises an error for investigation.
import sqlite3
import tempfile
from pathlib import Path
def settle(db, attempt, actual):
if not isinstance(actual, int) or actual < 0:
raise ValueError("actual must be nonnegative integer microdollars")
db.execute("BEGIN IMMEDIATE")
try:
row = db.execute(
"SELECT held, state, actual FROM jobs WHERE attempt = ?",
(attempt,),
).fetchone()
if row is None:
raise KeyError(attempt)
held, state, previous = row
if state == "settled":
if previous != actual:
raise ValueError("conflicting settlement requires review")
db.commit()
return
db.execute(
"UPDATE account SET spent = spent + ? WHERE id = 1", (actual,)
)
db.execute(
"UPDATE jobs SET held = 0, state = 'settled', actual = ? "
"WHERE attempt = ?", (actual, attempt)
)
db.commit()
except Exception:
db.rollback()
raise
def available(db):
cap, spent = db.execute(
"SELECT cap, spent FROM account WHERE id = 1"
).fetchone()
held = db.execute("SELECT COALESCE(SUM(held), 0) FROM jobs").fetchone()[0]
return cap - spent - held
with tempfile.TemporaryDirectory() as directory:
db = sqlite3.connect(Path(directory) / "settlement.sqlite")
db.executescript("""
CREATE TABLE account (
id INTEGER PRIMARY KEY, cap INTEGER, spent INTEGER
);
INSERT INTO account VALUES (1, 300000, 0);
CREATE TABLE jobs (
attempt TEXT PRIMARY KEY, held INTEGER NOT NULL,
state TEXT NOT NULL, actual INTEGER
);
INSERT INTO jobs VALUES ('a', 176000, 'submitting', NULL);
INSERT INTO jobs VALUES ('b', 100000, 'submitted', NULL);
""")
assert available(db) == 24000
with db:
db.execute("UPDATE jobs SET state = 'uncertain' WHERE attempt = 'a'")
assert available(db) == 24000 # Timeout does not free capacity.
settle(db, "b", 90000)
assert available(db) == 34000
settle(db, "b", 90000)
assert available(db) == 34000 # Duplicate cannot debit twice.
try:
settle(db, "b", 95000)
except ValueError:
pass
else:
raise AssertionError("conflicting settlement accepted")
settle(db, "a", 220000) # Record an overage without hiding liability.
assert available(db) == -10000
assert db.execute("SELECT spent FROM account").fetchone()[0] == 310000
db.close()
print("Settlement assertions passed")
Production settlement should retain an immutable audit history, evidence identifiers, currency, and price version. Authenticate incoming notifications and deduplicate their identifiers where notifications are supported. Correct later billing changes through explicit adjustment entries, not silent edits to previously settled records.
What operational checks keep reservations useful?
Monitor unresolved exposure and settlement discrepancies, not simply the current available balance.
Alert on aging uncertain attempts, outbox backlog, actual charges above reservations, conflicting settlement events, and cross-tenant identifier mistakes. Record reservation age separately from generation duration. An aging hold is an investigation signal, not permission to release money.
Test crashes before dispatch, during transmission, after acceptance, and during settlement. Also test duplicate queue deliveries, reordered notifications, database lock timeouts, and requests that change resolution after admission. Freeze immutable billable parameters; changing them requires a new or adjusted reservation before submission.
If reservation storage is unavailable, stop admitting new paid work. Continue recovery when safely possible, but do not switch to an unchecked balance read. This conservative failure behavior preserves the budget boundary while the ledger is repaired.
What questions arise when implementing budget reservations?
The recurring questions concern timeouts, retries, wallet balances, and differences between estimated and actual charges.
Can a reservation expire after a fixed timeout?
A reservation for provably undispatched work can be released if dispatch is atomically prevented. A potentially submitted attempt must retain its hold until evidence resolves the exposure. Elapsed time alone proves neither cancellation nor absence of a charge.
Does every retry need another reservation?
A new generation attempt needs additional capacity. Reprocessing the same local attempt should reuse its reservation. A network retry must follow documented submission semantics; local idempotency cannot prevent a remote duplicate by itself.
Is this the same as the GenRelay wallet balance?
No. This ledger allocates application budgets across projects and concurrent jobs. It does not modify provider balances or assert provider-side reservation support. Reconcile actual charges against available account records and ensure every application submission uses admission control.
What if the final charge is higher than the hold?
Record the full charge, flag the estimate mismatch, and block further admissions when capacity is exhausted. Investigate the pricing or request assumptions before accepting similar work again. Accounting accuracy takes precedence over preserving an incorrectly optimistic budget display.