AI Credits
Build model-weighted prepaid credits and weekly plus rolling-window limits with period rollover and rolling-hour restoration on Parix.
Overview
This manual implements two AI usage products on Parix:
- Model-weighted credits. A customer sees one common credit balance, while each model burns that balance at a different input-token, cached-token, output-token, or fixed-request rate. The application reserves the maximum possible burn before dispatch, then posts the provider's exact final usage and releases the unused reservation.
- Rolling window / weekly limit. A subscription includes usage up to both a weekly limit and a rolling five-hour limit. The application reserves the same worst-case usage against both dimensions before dispatch, posts the exact final usage to both, rotates the weekly allowance at its configured boundary, and replenishes rolling capacity as each usage charge ages out.
These are product patterns, not compatibility claims about OpenRouter, OpenAI, or any other provider. Your application owns the model catalog, provider integration, pricing and limit policies, token accounting, period clocks, plan entitlements, and customer display. Parix and TigerBeetle own integer balances, concurrent limit enforcement, pending-transfer lifecycle, atomic linked batches, and immutable usage movements.
Choose exactly one billing mode for each request before contacting the model provider. Do not silently charge credits after dual-limit admission fails unless the customer has explicitly enabled and accepted that fallback policy.
Architecture and ownership
| Component | Owns | Does not own |
|---|---|---|
| AI gateway | Authentication, model selection, prompt limits, provider dispatch, streaming, cancellation, and provider request ID | Authoritative credit or allowance mutation |
| Rating service | Immutable model-rate version, tokenizer/meter interpretation, integer reserve and actual calculations, and rounding | Provider execution or ledger persistence |
| Entitlement service | Customer mode, plan version, weekly policy, rolling-window policy, limit-reset orchestration, stable IDs, and state | Concurrency-safe balance enforcement |
| Parix API gateway | Credential and database checks, strict payload validation, plan enforcement, namespace mapping, and routing | Product identity, model pricing, or provider reconciliation |
| TigerBeetle | Credit balances, pending reservations, no-overdraft constraints, linked atomicity, and transfer-ID idempotency | Prompts, raw completions, rate catalogs, or wall-clock scheduling |
| Usage store and workflow | Raw usage, request/reset mappings, period accounts, due replenishments, retry state, and reconciliation | Rewriting committed ledger history |
A request follows this durable state machine:
- Resolve the customer's mode and pin a model-rate or allowance-policy version.
- Validate the maximum input and output bounds, then calculate the worst-case integer amount.
- Persist the request, every transfer ID, the exact intended payload, and the provider idempotency key.
- Look up the stable reservation IDs. Create the reservation only when none exists.
- Dispatch the provider request only after exact lookup proves that the reservation exists.
- Persist the provider's final, deduplicated usage event.
- Credit mode partially posts the exact charge or voids a zero/failed request.
- Dual-limit mode (weekly + rolling window) atomically partial-posts both exact deductions. A period workflow activates a newly funded weekly account at the weekly boundary, while a durable scheduler replenishes each rolling deduction five hours after its admission time.
- Reconcile every terminal, period-grant, reset-adjustment, and replenishment transfer by exact lookup before advancing its durable state.
A client disconnect is not proof that the model provider stopped. Query or reconcile the provider request before voiding a reservation. Never release the ledger guard merely because the HTTP stream ended.
Ledger model
Units and ledgers
| Ledger | Unit | Purpose |
|---|---|---|
7401 | Atomic prepaid-credit unit | Grants one displayed balance and burns it at model-specific rates |
7402 | Normalized weekly AI unit | Enforces the fixed weekly limit dimension with exact posted usage |
7403 | Normalized rolling AI unit | Enforces the rolling five-hour rolling-window dimension with posted usage and delayed replenishment |
The example defines one displayed credit as 1,000,000 atomic credit units. Never store a fractional TigerBeetle amount or calculate a burn through JavaScript Number. Keep the display scale and every model rate versioned in the application.
The two weekly and rolling limit ledgers intentionally use the same normalized unit so one request can deduct the same actual amount from both dimensions. If weekly and rolling limits use different units or weight schedules, calculate and reserve each independently instead of copying the amount.
A dual-limit policy may count raw tokens, model-weighted tokens, requests, or another normalized compute unit. Version that allowance weight table independently from prepaid-credit prices: changing a model's monetary burn rate must not silently change a subscriber's weekly or rolling entitlement. Apply the same integer, worst-case-reserve, and single-rounding rules to whichever allowance unit the plan promises.
Accounts
history is account flag 8; debits_must_not_exceed_credits is flag 2. Customer balances and allowance accounts use both (10) so concurrent requests cannot overdraw them.
| Role | Example ID | Ledger | Code | Flags | Meaning |
|---|---|---|---|---|---|
| Credit grant source | 95000000000000000001 | 7401 | 100 | 8 | Governed source for purchased or promotional credits |
| Customer credit balance | 95000000000000000002 | 7401 | 110 | 10 | Available prepaid atomic credits |
| Credit usage sink | 95000000000000000003 | 7401 | 120 | 10 | Posted burn and constrained source for reviewed credit corrections |
| Weekly allowance source | 95000000000000000004 | 7402 | 200 | 8 | Governed source for weekly allowance grants |
| Customer weekly allowance | 95000000000000000005 | 7402 | 210 | 10 | One customer's capacity for one pinned weekly period |
| Weekly usage sink | 95000000000000000006 | 7402 | 220 | 8 | Counterparty for weekly pending and posted usage |
| Rolling allowance source | 95000000000000000007 | 7403 | 300 | 8 | Governed source for rolling-window capacity |
| Customer rolling allowance | 95000000000000000008 | 7403 | 310 | 10 | Five-hour capacity less active usage reservations |
| Customer rolling usage sink | 95000000000000000009 | 7403 | 320 | 10 | Holds this customer's posted usage and funds exact delayed replenishment |
The unconstrained source accounts are modeling boundaries. Application authorization and reconciliation must govern every grant. A usage sink is a quantity account, not revenue and not a receivable. Pair each customer's rolling allowance with a constrained rolling sink; this prevents replenishment from returning more normalized usage than that customer's prior rolling posts supplied in aggregate.
For each fixed or customer-anchored week, create or select a customer weekly account for that plan period and fund it once. Pin an admitted request to that account even if settlement crosses the reset. New requests after the reset use a newly funded account. Keep the old account and transfers for history; do not replenish posted weekly usage. A reviewed same-week manual reset creates a new account generation tied to its reset case instead of topping up the old account. If the product instead promises a rolling seven-day window, use the rolling replenishment pattern with a seven-day delay and document that different policy explicitly.
Transfer codes
| Code | Event | Lifecycle |
|---|---|---|
1000 | Credit grant | Posted source-to-customer transfer |
1010 | Credit usage | Pending maximum, then partial/full post or void |
1020 | Credit refund or correction | New reviewed compensating transfer |
2000 | Weekly allowance grant | Posted source-to-customer transfer |
2010 | Weekly usage | Pending estimate, then exact partial/full post or void |
3000 | Rolling allowance grant | Posted source-to-customer transfer |
3010 | Rolling usage | Pending estimate, then exact partial/full post or void |
3020 | Rolling replenishment | Posted usage-sink-to-customer transfer after five hours |
9000 | Reviewed adjustment | New governed transfer with an external case |
Model-weighted credit calculation
Store each rate as atomic credit units per one million provider-reported units. A rate row includes provider, model, meter kind, effective interval, immutable version, and rounding policy. This example rate version charges:
- input:
200,000atomic units per one million tokens; - output:
800,000atomic units per one million tokens.
For 2,000 input tokens and a maximum 1,000 output tokens, reserve:
ceil((2,000 × 200,000 + 1,000 × 800,000) / 1,000,000) = 1,200 atomic unitsIf the provider reports 400 output tokens, post:
ceil((2,000 × 200,000 + 400 × 800,000) / 1,000,000) = 720 atomic unitsThe partial post is terminal: it posts 720 and releases the unused 480. Round once after summing all meter numerators. Persist raw meter counts and the pinned rate version outside TigerBeetle. Use only opaque non-secret correlations in user_data_*.
Weekly and rolling-window reservation and replenishment lifecycle
Before provider dispatch, submit one linked pair:
- weekly estimate:
pending | linked(3), with a short provider-execution TTL; - rolling estimate:
pending(2), with the same provider-execution TTL.
The pair commits only when both constrained allowance accounts can cover the same worst-case usage. Each transfer stays within its own ledger even though the linked chain spans two ledgers.
When actual usage A is known and 0 < A <= estimate, partial-post both pending transfers in one linked pair. The weekly post remains on the account for its pinned week. The rolling post remains deducted until a separate stable transfer moves exactly A from the constrained rolling sink back to the customer account at admitted_at + 18,000 seconds.
The rolling account therefore has this available capacity:
limit - unreplenished posted usage - in-flight pending reservationsThis is a true rolling window rather than a tumbling five-hour bucket. Use the exact TigerBeetle timestamp returned by lookup of the original rolling reservation as admitted_at. Persist one replenishment job and one replenishment transfer ID for each confirmed rolling post. Never replenish early; a delayed scheduler run is conservative. Arm the job only after exact lookup proves the post exists, and use an application unique constraint so the post can fund one replenishment only.
For customer percentages, calculate each account's available amount as credits_posted - debits_posted - debits_pending, then display used = limit - available against the governed limit. Use the current pinned weekly account for the weekly percentage and the long-lived rolling account for the five-hour percentage. Keep the calculation in integer or rational arithmetic and label the reset/due time from application policy; do not infer a clock from balance fields.
If actual usage is zero, void both estimates in a linked pair. The pending timeout is only a safety TTL for in-flight provider work; it is not the five-hour window or the weekly reset. An expired reservation cannot be posted, and its provider request still needs reconciliation.
This explicit terminal lifecycle also avoids relying on natural expiry for long-lived Shared reservations. Shared counts open pending transfers from submitted pending/post/void payloads, so always settle or void promptly and monitor that counter. Production still requires an approved production topology and current plan-limit review.
Limit reset orchestration
Treat limit reset as an application-owned durable workflow. Parix has no product-level reset clock or reset endpoint. Persist one reset record before any write with an opaque reset ID, customer and policy version, target (weekly, rolling, or both), source (scheduler or authorized operator), reason, effective time, period anchor, every account and transfer ID, exact payload hashes, and workflow state. Serialize reset and replenishment work per customer so two workers cannot return the same capacity differently.
The normal clock behavior depends on the selected dimension:
| Target | Normal behavior |
|---|---|
weekly | At the configured boundary, create or select the new period account, submit its one stable allowance grant, prove the account and grant by exact lookup, then route new admissions to it. The rolling account is unchanged. |
rolling | There is no global five-hour reset. Run each due 3020 replenishment once when its original reservation timestamp reaches five hours. The active weekly account is unchanged. |
both | Perform the weekly period rotation and any independently due rolling replenishments under one durable operation. Their clocks remain independent; “both” does not turn the rolling window into a bucket. |
A scheduled weekly job may run early to provision the next account, but it must not activate that account before the policy boundary. Derive the account ID and grant ID from the customer, policy version, and anchored period, or store equally stable random IDs before submission. A duplicate or late job looks up and reuses those exact objects. Requests admitted before the boundary remain pinned to the old weekly account and may settle there after activation of the new one.
For an authorized same-week weekly reset, create a fresh account generation and grant instead of mutating or topping up the current account. Include the reset case in the application uniqueness key. Activate the new generation only after exact account and grant lookup. Preserve the prior account, unused allowance, posted usage, and in-flight reservations for audit; they do not carry into new admissions.
A true rolling window restores capacity continuously, not all at once. If support policy permits an exceptional early rolling reset, model it as a reviewed correction rather than an ordinary 3020 replenishment:
- Gate new dual-limit admission for the customer and acquire the same lock used by the replenishment scheduler.
- Enumerate confirmed rolling posts that have neither a successful
3020replenishment nor an earlier reset adjustment. Do not calculate the reset from the account balance alone. - For each selected post, persist one stable
9000adjustment from that customer's constrained rolling sink back to the rolling account for exactly the post's still-unrestored amount. Key the application mapping uniquely by original rolling post. - Exact-lookup every adjustment, then mark its normal replenishment record
satisfied_by_adjustment. The later3020job must reconcile and no-op; submitting both would return capacity twice. - Mark the reset complete and reopen admission only after every selected object reconciles exactly. Retry a partial or ambiguous workflow with the same IDs and payloads.
An early rolling reset does not close an in-flight pending reservation. After all posted usage is restored, available rolling capacity is still limit - in-flight pending reservations. To promise a completely unused limit, first reconcile the provider and explicitly post or void each affected weekly-and-rolling pending pair; never void live work merely to make a reset display reach zero.
For both, run the weekly account generation and rolling per-post adjustments as one reset saga. Pause new dual-limit admission until all selected legs and the new weekly pointer are reconciled. TigerBeetle transfers may share an intentional linked chain when the complete chain fits the active event limit, but account creation, SQL state, and routing activation are still not cross-system atomic. A recovery worker must resume the persisted saga rather than allocate replacement IDs.
Display a weekly Resets at timestamp from the pinned policy boundary. For rolling capacity, display the earliest Capacity returns at timestamp among unreplenished posts; do not show one misleading five-hour reset clock. Show operator resets as audited adjustments in activity history.
Invariants
| Invariant | Enforcement |
|---|---|
| One request uses one mode | Persist mode before reservation; never debit both credit and dual-limit products implicitly |
| Rates are deterministic | Pin an immutable rate version and use integer arithmetic with one documented rounding step |
| Provider work starts only after admission | Exact lookup proves every reservation exists before dispatch |
| Credits cannot overdraw | Reserve the worst-case atomic burn from a constrained customer balance |
| Both weekly and rolling limits admit together | Create weekly and rolling reservations as one linked chain |
| Actual usage cannot exceed reserve | Enforce provider input/output caps; route any overage to incident review rather than inventing a larger post |
| The rolling window is truly sliding | Replenish each exact rolling post once, and only at its reservation timestamp plus five hours |
| One pending has one terminal outcome | Assign stable IDs for post and void alternatives and serialize terminal state in the application |
| Retries cannot duplicate usage | Persist IDs and exact payload hashes before write; reconcile all immutable fields before replay |
| Corrections remain explainable | Use a new refund or adjustment transfer; never rewrite a prior burn or usage record |
Before you begin
You need:
- a Parix database in Ready state and its immutable database UUID;
- an approved production topology for live weekly and rolling-window limits;
- a specific-database API key with
db:readanddb:writefor the service; - governed ledger IDs, account codes, transfer codes, and non-PII stable-ID allocation;
- a versioned model/rate catalog and provider meter contract;
- explicit maximum input, output, tool, image, audio, cache, and fixed-request bounds where applicable;
- a durable request table that stores mode, policy/rate version, provider ID, every ledger ID and payload hash, admission time, pinned weekly account, rolling replenishment due time, final usage, and state;
- a durable replenishment scheduler keyed uniquely to each confirmed rolling post;
- a durable reset table and per-customer lock for weekly period rotation and any authorized
weekly,rolling, orbothoperator workflow; - a policy for provider failure, stream disconnect, late usage, zero usage, overage, refund, plan change, and weekly boundary handling; and
- reconciliation ownership for credit grants, open reservations, final usage, due replenishments, weekly periods, and reviewed adjustments.
Developer is shared and quota-limited. Dedicated Single Node is isolated but non-HA and non-production. Use an approved Production or Enterprise topology for production workloads and confirm the active plans and limits.
The strict public payload schema accepts up to 8,190 items, but the active plan may allow fewer events per request. Never split one linked chain to work around a plan limit. Raw JSON should encode wide IDs and amounts as decimal strings.
Dashboard walkthrough
- Open the intended organization and database. Confirm Ready, topology, database UUID, and current pending-transfer limits.
- Select Connect and create a Specific database API key for the AI gateway. Store the one-time secret in server-side secret management.
- Open Query, choose Create accounts, and enter the account payload only in an isolated test database.
- Review every ID, ledger, code, and flag. In particular, customer balances and allowance accounts must use flags
10. - After a test grant or reservation, choose Lookup transfers and query the exact stable IDs. Do not infer success from the AI request row alone.
- Choose Query accounts with an explicit ledger on Shared. Compare posted and pending balances with the request and replenishment state tables.
- Treat a reset as an application workflow, not a Dashboard balance edit. Use Query only to inspect the exact period grants, due replenishments, or reviewed adjustments created by that workflow.

The Query explorer writes directly to the selected database. It does not create model rates, plan entitlements, provider requests, or window schedules in your application.

Query each ledger separately. Pending reservations and posted deductions reduce live capacity; model rates, reset clocks, and replenishment schedules remain application-owned records.
Live-write warning:
create_accountsandcreate_transferspersist immediately. Verify the environment, database UUID, stable IDs, exact array order, amounts, timeouts, ledgers, codes, and flags before selecting Run. A linked-chain mistake can reject the entire chain; an unlinked batch can partially succeed.
CLI walkthrough
The examples use the latest published @parix/cli package. CLI OAuth is for authorized human operators; unattended AI services use a scoped API key.
npm install --global @parix/cli@latest
parix auth login
parix auth status
parix database list --json
export PARIX_DATABASE_ID="db_replace_with_uuid"Create ai-credit-accounts.json as a bare JSON array:
[
{ "id": "95000000000000000001", "ledger": 7401, "code": 100, "flags": 8 },
{ "id": "95000000000000000002", "ledger": 7401, "code": 110, "flags": 10 },
{ "id": "95000000000000000003", "ledger": 7401, "code": 120, "flags": 10 },
{ "id": "95000000000000000004", "ledger": 7402, "code": 200, "flags": 8 },
{ "id": "95000000000000000005", "ledger": 7402, "code": 210, "flags": 10 },
{ "id": "95000000000000000006", "ledger": 7402, "code": 220, "flags": 8 },
{ "id": "95000000000000000007", "ledger": 7403, "code": 300, "flags": 8 },
{ "id": "95000000000000000008", "ledger": 7403, "code": 310, "flags": 10 },
{ "id": "95000000000000000009", "ledger": 7403, "code": 320, "flags": 10 }
]parix tb create-accounts "$PARIX_DATABASE_ID" --file ./ai-credit-accounts.json --jsonFund one displayed credit, a 100,000-unit weekly allowance, and a 20,000-unit rolling allowance. Production grants require your own purchase, subscription, or entitlement evidence.
[
{
"id": "95100000000000000001",
"debit_account_id": "95000000000000000001",
"credit_account_id": "95000000000000000002",
"amount": "1000000",
"ledger": 7401,
"code": 1000,
"flags": 0
},
{
"id": "95100000000000000002",
"debit_account_id": "95000000000000000004",
"credit_account_id": "95000000000000000005",
"amount": "100000",
"ledger": 7402,
"code": 2000,
"flags": 0
},
{
"id": "95100000000000000003",
"debit_account_id": "95000000000000000007",
"credit_account_id": "95000000000000000008",
"amount": "20000",
"ledger": 7403,
"code": 3000,
"flags": 0
}
]Save that array as ai-entitlement-grants.json. Multi-item grants stay in a reviewed file. Submit the full batch and look up all three IDs. The transfers are independent grants, not one linked business event:
parix tb create-transfers "$PARIX_DATABASE_ID" --file ./ai-entitlement-grants.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 95100000000000000001 \
--id 95100000000000000002 \
--id 95100000000000000003 \
--jsonA single-leg grant can instead use flag-driven fields. Leave user_data_* at zero unless you will query by an entitlement or purchase correlation; the transfer id is the idempotency key. Do not submit both this form and the multi-item file for the same stable ID:
parix tb create-transfers "$PARIX_DATABASE_ID" \
--id 95100000000000000001 \
--from 95000000000000000001 \
--to 95000000000000000002 \
--amount 1000000 \
--ledger 7401 \
--code 1000 \
--jsonReserve and settle a credit request
Save the 1,200-atomic maximum as credit-reserve.json. pending is flag 2; timeout is an in-flight reservation TTL in seconds, not the product window.
[
{
"id": "95100000000000000010",
"debit_account_id": "95000000000000000002",
"credit_account_id": "95000000000000000003",
"amount": "1200",
"pending_id": "0",
"user_data_128": "95200000000000000001",
"user_data_64": "953000000000000001",
"user_data_32": 1,
"timeout": 600,
"ledger": 7401,
"code": 1010,
"flags": 2
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./credit-reserve.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" --id 95100000000000000010 --jsonAfter the provider's final usage is durably stored, save this 720-atomic partial post as credit-post.json. The new transfer copies the original external accounts, ledger, code, and correlation; post_pending_transfer is flag 4.
[
{
"id": "95100000000000000011",
"debit_account_id": "95000000000000000002",
"credit_account_id": "95000000000000000003",
"amount": "720",
"pending_id": "95100000000000000010",
"user_data_128": "95200000000000000001",
"user_data_64": "953000000000000001",
"user_data_32": 1,
"timeout": 0,
"ledger": 7401,
"code": 1010,
"flags": 4
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./credit-post.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" --id 95100000000000000011 --jsonUse an explicit void (flags: 8) with the original 1,200 amount when provider execution definitively produced zero billable usage. Do not use a zero post.
Reserve both weekly and rolling limits
Save dual-limit-reserve.json. This example reserves 4,000 normalized units from both dimensions for up to ten minutes of provider execution. The weekly leg is linked and the rolling leg closes the chain. The reservation timestamp returned by lookup anchors the later five-hour replenishment.
[
{
"id": "95100000000000000020",
"debit_account_id": "95000000000000000005",
"credit_account_id": "95000000000000000006",
"amount": "4000",
"pending_id": "0",
"user_data_128": "95200000000000000002",
"user_data_64": "953000000000000002",
"user_data_32": 1,
"timeout": 600,
"ledger": 7402,
"code": 2010,
"flags": 3
},
{
"id": "95100000000000000021",
"debit_account_id": "95000000000000000008",
"credit_account_id": "95000000000000000009",
"amount": "4000",
"pending_id": "0",
"user_data_128": "95200000000000000002",
"user_data_64": "953000000000000002",
"user_data_32": 2,
"timeout": 600,
"ledger": 7403,
"code": 3010,
"flags": 2
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./dual-limit-reserve.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 95100000000000000020 \
--id 95100000000000000021 \
--jsonAssume the provider reports 2,400 normalized units. Save dual-limit-settle.json. The weekly post is linked and the rolling post closes the pair. Each partial post is terminal: it posts 2,400 and releases the unused 1,600 from its estimate.
[
{
"id": "95100000000000000022",
"debit_account_id": "95000000000000000005",
"credit_account_id": "95000000000000000006",
"amount": "2400",
"pending_id": "95100000000000000020",
"user_data_128": "95200000000000000002",
"user_data_64": "953000000000000002",
"user_data_32": 1,
"timeout": 0,
"ledger": 7402,
"code": 2010,
"flags": 5
},
{
"id": "95100000000000000023",
"debit_account_id": "95000000000000000008",
"credit_account_id": "95000000000000000009",
"amount": "2400",
"pending_id": "95100000000000000021",
"user_data_128": "95200000000000000002",
"user_data_64": "953000000000000002",
"user_data_32": 2,
"timeout": 0,
"ledger": 7403,
"code": 3010,
"flags": 4
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./dual-limit-settle.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 95100000000000000022 \
--id 95100000000000000023 \
--jsonAfter exact lookup proves transfer 95100000000000000023 posted, persist one replenishment job due exactly five hours after the original rolling reservation timestamp. When due, save and submit rolling-replenish.json:
[
{
"id": "95100000000000000024",
"debit_account_id": "95000000000000000009",
"credit_account_id": "95000000000000000008",
"amount": "2400",
"pending_id": "0",
"user_data_128": "95200000000000000002",
"user_data_64": "953000000000000002",
"user_data_32": 3,
"timeout": 0,
"ledger": 7403,
"code": 3020,
"flags": 0
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./rolling-replenish.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" --id 95100000000000000024 --jsonThe ten-minute reservation TTL is illustrative. It must cover expected provider finalization and stay within the current plan limit. It does not define either customer-visible window.
Reset weekly, rolling, or both limits
The following payloads demonstrate the ledger legs of a reset workflow in an isolated database. A production service must first persist the reset record, hold the customer reset/replenishment lock, and gate dual-limit admission. Direct CLI commands cannot provide those application guarantees.
For a normal weekly rollover or an authorized same-week reset, create a fresh account. This example uses account 95000000000000000010 as the next period or reset generation:
[
{
"id": "95000000000000000010",
"user_data_128": "95400000000000000001",
"user_data_64": "953000000000000002",
"user_data_32": 1,
"ledger": 7402,
"code": 210,
"flags": 10
}
]parix tb create-accounts "$PARIX_DATABASE_ID" --file ./weekly-next-period-account.json --json
parix tb lookup-accounts "$PARIX_DATABASE_ID" --id 95000000000000000010 --jsonFund that account once with the configured 100,000-unit weekly allowance. The reset workflow must activate the application pointer only after both the account and this exact grant reconcile:
[
{
"id": "95100000000000000030",
"debit_account_id": "95000000000000000004",
"credit_account_id": "95000000000000000010",
"amount": "100000",
"pending_id": "0",
"user_data_128": "95400000000000000001",
"user_data_64": "953000000000000002",
"user_data_32": 1,
"timeout": 0,
"ledger": 7402,
"code": 2000,
"flags": 0
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./weekly-next-period-grant.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" --id 95100000000000000030 --jsonFor normal rolling restoration, continue to use rolling-replenish.json only when the original post is due. There is no bulk rolling reset payload. If an authorized operator must restore the example's 2,400 posted units early, serialize against the normal scheduler and persist one adjustment tied to original rolling post 95100000000000000023:
[
{
"id": "95100000000000000031",
"debit_account_id": "95000000000000000009",
"credit_account_id": "95000000000000000008",
"amount": "2400",
"pending_id": "0",
"user_data_128": "95400000000000000001",
"user_data_64": "953000000000000002",
"user_data_32": 2,
"timeout": 0,
"ledger": 7403,
"code": 9000,
"flags": 0
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./rolling-early-reset-adjustment.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" --id 95100000000000000031 --jsonAfter exact lookup, mark the original post's replenishment satisfied_by_adjustment. Do not later submit transfer 95100000000000000024; it is the mutually exclusive normal replenishment for the same posted usage. A real customer with multiple unreplenished posts needs one uniquely mapped adjustment per post, not one aggregate grant.
For a weekly reset, submit and activate only the new weekly generation. For a rolling reset, keep the current weekly pointer and restore only selected rolling posts. For both, reconcile both sets under the same durable reset ID and keep admission paused until the reset record is complete; do not treat two successful CLI commands as an atomic cross-system operation.
Node.js implementation
@parix/tigerbeetle-node is the published Node.js client for TigerBeetle-shaped operations on the Parix HTTPS gateway. It does not open a native TigerBeetle connection. Pin a package version approved for your environment, configure { baseUrl, apiKey, databaseId } with a database-scoped API key, keep the client behind an application boundary, and validate behavior before production traffic.
import type { CreateTransferResult, Transfer } from '@parix/tigerbeetle-node';
import { CreateTransferError, TransferFlags, amount_max, createClient } from '@parix/tigerbeetle-node';
const client = createClient({
baseUrl: process.env.PARIX_BASE_URL ?? 'https://parix.io',
apiKey: process.env.PARIX_API_KEY!,
databaseId: process.env.PARIX_DATABASE_ID!,
});
const accounts = {
creditCustomer: 95000000000000000002n,
creditSink: 95000000000000000003n,
weeklyCustomer: 95000000000000000005n,
weeklySink: 95000000000000000006n,
rollingCustomer: 95000000000000000008n,
rollingSink: 95000000000000000009n,
} as const;
type Meter = { units: bigint; atomicPerMillion: bigint };
function divCeil(value: bigint, denominator: bigint): bigint {
if (value < 0n || denominator <= 0n) throw new Error('invalid non-negative fixed-point input');
return (value + denominator - 1n) / denominator;
}
function calculateCreditBurn(meters: readonly Meter[]): bigint {
const numerator = meters.reduce((sum, meter) => sum + meter.units * meter.atomicPerMillion, 0n);
return divCeil(numerator, 1_000_000n);
}
function sameTransfer(actual: Transfer, intended: Transfer): boolean {
return (
actual.id === intended.id &&
actual.debit_account_id === intended.debit_account_id &&
actual.credit_account_id === intended.credit_account_id &&
actual.amount === intended.amount &&
actual.pending_id === intended.pending_id &&
actual.user_data_128 === intended.user_data_128 &&
actual.user_data_64 === intended.user_data_64 &&
actual.user_data_32 === intended.user_data_32 &&
actual.timeout === intended.timeout &&
actual.ledger === intended.ledger &&
actual.code === intended.code &&
actual.flags === intended.flags
);
}
async function lookupExactBatch(batch: readonly Transfer[]): Promise<'none' | 'exact'> {
const found = await client.lookupTransfers(batch.map(({ id }) => id));
if (found.length === 0) return 'none';
if (found.length !== batch.length) throw new Error('partial ledger visibility: reconcile manually');
const byId = new Map(found.map((transfer) => [transfer.id, transfer]));
if (
!batch.every((intended) => {
const actual = byId.get(intended.id);
return actual !== undefined && sameTransfer(actual, intended);
})
) {
throw new Error('stable ID exists with different immutable fields');
}
return 'exact';
}
class TransferBatchRejected extends Error {
constructor(readonly results: readonly CreateTransferResult[]) {
const details = results
.map(({ index, result }) => {
const name =
result === CreateTransferError.exceeds_credits
? 'exceeds_credits'
: result === CreateTransferError.exceeds_debits
? 'exceeds_debits'
: result === CreateTransferError.linked_event_failed
? 'linked_event_failed'
: String(result);
return `${index}:${name}`;
})
.join(', ');
super(`TigerBeetle rejected transfer indexes ${details}`);
}
}
async function createOrReconcile(batch: readonly Transfer[]): Promise<void> {
if ((await lookupExactBatch(batch)) === 'exact') return;
let results: CreateTransferResult[];
try {
results = await client.createTransfers([...batch]);
} catch (error) {
// Timeout, connection loss, and 5xx outcomes are ambiguous. The lookup below
// decides whether the exact batch committed before any identical retry.
if ((await lookupExactBatch(batch)) === 'exact') return;
throw error;
}
if ((await lookupExactBatch(batch)) === 'exact') return;
if (results.length > 0) throw new TransferBatchRejected(results);
throw new Error('create returned without an exact durable batch');
}The adapter returns decoded TigerBeetle result arrays and does not expose the raw gateway's persisted envelope. On write conflicts that include tbResults, HTTP 409 is unwrapped into the same sparse result array rather than thrown. Therefore every write path performs exact lookup before advancing. A nonempty create result also requires lookup; classify every indexed result after reconciliation (for example CreateTransferError.exceeds_credits plus linked-neighbor failures is a capacity decline, while schema and stable-ID conflicts are defects or incidents). Never interpret only the first conflict entry.
Orchestrate a reset workflow
The application database must serialize resets with normal rolling replenishment. This sketch assumes the reset plan, account object, transfer objects, and payload hashes were persisted before the worker started. ensureWeeklyAccountExact must create-or-lookup the exact new period account with the same rigor as createOrReconcile applies to transfers.
type ResetTarget = 'weekly' | 'rolling' | 'both';
type LimitResetPlan = {
resetId: bigint;
customerId: bigint;
target: ResetTarget;
weekly?: {
accountId: bigint;
ensureWeeklyAccountExact: () => Promise<void>;
grant: Transfer;
};
rolling?: readonly {
originalPostId: bigint;
adjustment: Transfer;
}[];
};
type ResetCoordinator = {
acquireCustomerLock(customerId: bigint): Promise<() => Promise<void>>;
pauseDualLimitAdmission(customerId: bigint, resetId: bigint): Promise<void>;
claimRollingPost(
resetId: bigint,
originalPostId: bigint,
adjustmentId: bigint,
): Promise<'claimed' | 'already-restored'>;
markRollingPostAdjusted(resetId: bigint, originalPostId: bigint, adjustmentId: bigint): Promise<void>;
completeAndActivate(resetId: bigint, weeklyAccountId?: bigint): Promise<void>;
markBlocked(resetId: bigint, error: unknown): Promise<void>;
resumeDualLimitAdmission(customerId: bigint, resetId: bigint): Promise<void>;
};
async function executeLimitReset(coordinator: ResetCoordinator, plan: LimitResetPlan): Promise<void> {
const includesWeekly = plan.target === 'weekly' || plan.target === 'both';
const includesRolling = plan.target === 'rolling' || plan.target === 'both';
if (includesWeekly !== (plan.weekly !== undefined)) throw new Error('weekly reset plan mismatch');
if (includesRolling !== (plan.rolling !== undefined)) throw new Error('rolling reset plan mismatch');
const unlock = await coordinator.acquireCustomerLock(plan.customerId);
try {
await coordinator.pauseDualLimitAdmission(plan.customerId, plan.resetId);
if (plan.weekly !== undefined) {
await plan.weekly.ensureWeeklyAccountExact();
await createOrReconcile([plan.weekly.grant]);
}
for (const restoration of plan.rolling ?? []) {
const claim = await coordinator.claimRollingPost(
plan.resetId,
restoration.originalPostId,
restoration.adjustment.id,
);
if (claim === 'already-restored') continue;
await createOrReconcile([restoration.adjustment]);
await coordinator.markRollingPostAdjusted(plan.resetId, restoration.originalPostId, restoration.adjustment.id);
}
await coordinator.completeAndActivate(plan.resetId, plan.weekly?.accountId);
await coordinator.resumeDualLimitAdmission(plan.customerId, plan.resetId);
} catch (error) {
await coordinator.markBlocked(plan.resetId, error);
// Keep admission paused. A recovery worker resumes this same plan and IDs.
throw error;
} finally {
await unlock();
}
}The claimRollingPost transaction must share a uniqueness boundary with the normal scheduler. It should return already-restored when an exact 3020 replenishment or prior reset adjustment already owns the source post. completeAndActivate verifies every selected ledger object again, marks the reset complete, and atomically changes the application weekly pointer. Never infer completion from the absence of an exception.
Construct a credit reservation and its terminal record from the same persisted request and rate version:
const requestId = 95200000000000000001n;
const rateVersionId = 953000000000000001n;
const creditReserveId = 95100000000000000010n;
const maximumBurn = calculateCreditBurn([
{ units: 2_000n, atomicPerMillion: 200_000n },
{ units: 1_000n, atomicPerMillion: 800_000n },
]);
const creditReservation: Transfer = {
id: creditReserveId,
debit_account_id: accounts.creditCustomer,
credit_account_id: accounts.creditSink,
amount: maximumBurn,
pending_id: 0n,
user_data_128: requestId,
user_data_64: rateVersionId,
user_data_32: 1,
timeout: 600,
ledger: 7401,
code: 1010,
flags: TransferFlags.pending,
timestamp: 0n,
};
await createOrReconcile([creditReservation]);
// Dispatch only after the exact reservation lookup above succeeds.
const actualBurn = calculateCreditBurn([
{ units: 2_000n, atomicPerMillion: 200_000n },
{ units: 400n, atomicPerMillion: 800_000n },
]);
if (actualBurn < 0n || actualBurn > maximumBurn) {
throw new Error('actual credit burn is outside the reserved amount');
}
// Full void of an unposted reservation uses the reserved amount (or amount_max for remaining).
const creditTerminal: Transfer =
actualBurn === 0n
? {
...creditReservation,
id: 95100000000000000012n,
amount: amount_max, // full remaining; reserved maximumBurn is also valid for unposted full void
pending_id: creditReserveId,
timeout: 0,
flags: TransferFlags.void_pending_transfer,
}
: {
...creditReservation,
id: 95100000000000000011n,
amount: actualBurn,
pending_id: creditReserveId,
timeout: 0,
flags: TransferFlags.post_pending_transfer,
};
await createOrReconcile([creditTerminal]);Reserve weekly and rolling capacity atomically, then partial-post the same actual usage to both dimensions. The application allocates and persists every shown ID before either function runs.
const linked = TransferFlags.linked;
const pending = TransferFlags.pending;
const postPending = TransferFlags.post_pending_transfer;
const voidPending = TransferFlags.void_pending_transfer;
const dualLimitRequestId = 95200000000000000002n;
const policyVersionId = 953000000000000002n;
const weeklyEstimateId = 95100000000000000020n;
const rollingEstimateId = 95100000000000000021n;
const estimate = 4_000n;
const weeklyEstimate: Transfer = {
id: weeklyEstimateId,
debit_account_id: accounts.weeklyCustomer,
credit_account_id: accounts.weeklySink,
amount: estimate,
pending_id: 0n,
user_data_128: dualLimitRequestId,
user_data_64: policyVersionId,
user_data_32: 1,
timeout: 600,
ledger: 7402,
code: 2010,
flags: pending | linked,
timestamp: 0n,
};
const rollingEstimate: Transfer = {
...weeklyEstimate,
id: rollingEstimateId,
debit_account_id: accounts.rollingCustomer,
credit_account_id: accounts.rollingSink,
user_data_32: 2,
timeout: 600,
ledger: 7403,
code: 3010,
flags: pending,
};
await createOrReconcile([weeklyEstimate, rollingEstimate]);
// Dispatch only after exact lookup proves both linked reservations exist.
const actual = 2_400n;
if (actual < 0n || actual > estimate) {
throw new Error('actual usage is outside the reserved amount');
}
const dualLimitSettlement: Transfer[] =
actual === 0n
? [
{
...weeklyEstimate,
id: 95100000000000000025n,
amount: amount_max, // full remaining; estimate is also valid for unposted full void
pending_id: weeklyEstimateId,
timeout: 0,
flags: voidPending | linked,
},
{
...rollingEstimate,
id: 95100000000000000026n,
amount: amount_max,
pending_id: rollingEstimateId,
timeout: 0,
flags: voidPending,
},
]
: [
{
...weeklyEstimate,
id: 95100000000000000022n,
amount: actual,
pending_id: weeklyEstimateId,
timeout: 0,
flags: postPending | linked,
},
{
...rollingEstimate,
id: 95100000000000000023n,
amount: actual,
pending_id: rollingEstimateId,
timeout: 0,
flags: postPending,
},
];
await createOrReconcile(dualLimitSettlement);
if (actual > 0n) {
const [persistedRollingReservation] = await client.lookupTransfers([rollingEstimateId]);
if (persistedRollingReservation === undefined || !sameTransfer(persistedRollingReservation, rollingEstimate)) {
throw new Error('rolling reservation timestamp is unavailable or mismatched');
}
const fiveHoursNanos = 18_000n * 1_000_000_000n;
const replenishmentDueAtNanos = persistedRollingReservation.timestamp + fiveHoursNanos;
const replenishment: Transfer = {
id: 95100000000000000024n,
debit_account_id: accounts.rollingSink,
credit_account_id: accounts.rollingCustomer,
amount: actual,
pending_id: 0n,
user_data_128: dualLimitRequestId,
user_data_64: policyVersionId,
user_data_32: 3,
timeout: 0,
ledger: 7403,
code: 3020,
flags: 0,
timestamp: 0n,
};
// Persist replenishmentDueAtNanos and this exact transfer as one durable job.
async function runDueReplenishment(nowNanos: bigint): Promise<void> {
if (nowNanos < replenishmentDueAtNanos) {
throw new Error('rolling replenishment is not due');
}
await createOrReconcile([replenishment]);
}
// A durable scheduler invokes runDueReplenishment(nowNanos) at or after dueAt.
}For zero actual usage, send only a linked weekly void and final rolling void, and create no replenishment job. For a provider failure with unknown usage, do not void until provider reconciliation proves that no billable usage can arrive. Always call client.destroy() in a finally block when the worker lifecycle ends.
Failure and retry handling
| Signal | Meaning | Required action |
|---|---|---|
Empty create result [] | Adapter reported no item conflicts | Still perform exact lookup before advancing the durable request state. The adapter does not expose persisted. |
| Nonempty create result | One or more TigerBeetle create conflicts | Map indexes and classify each result. HTTP 409 + tbResults is unwrapped into this array. Look up every stable ID; accept only a complete exact prior batch |
| Constraint rejection | Credit, weekly, or rolling capacity is insufficient | Return a product decline; do not retry as infrastructure failure or dispatch the provider |
Timeout, connection reset, 500, or 503 | Outcome may be absent or committed | Exact lookup first; retry only the identical payload and IDs when all are absent |
400 | Strict schema or deterministic request error | Fix the payload; do not retry unchanged |
401 or 403 | Credential, scope, database boundary, ledger, timeout, or plan policy rejected the request | Correct access or configuration; do not bypass it with operator credentials |
402 | Billing state blocks the operation | Restore billing and re-drive the same persisted intent after lookup |
429 | Admission pressure or hard plan quota | Distinguish transient pressure from durable quota; preserve IDs and never hot-loop |
| Partial ID visibility or field mismatch | Stable-ID contract or batch atomicity is broken | Stop automation, quarantine the request, and reconcile manually |
| Post/void says pending expired | Terminal usage arrived after reservation TTL | Reconcile provider truth; never create a replacement authorization only to hide the lapse |
| Actual usage exceeds reserve | Provider or bound enforcement violated policy | Stop automatic settlement, preserve evidence, and route to overage review |
| Stream disconnected | Only the client connection ended | Query provider execution; keep the reservation until final usage or definitive cancellation |
| Replenishment job is late | Rolling capacity remains lower than promised | Run the same stable replenishment after exact lookup; lateness is conservative, but alert on scheduler lag |
| Replenishment would run early | A scheduler or clock bug could grant excess capacity | Reject it before the ledger write and preserve the original reservation timestamp as evidence |
| Weekly rollover job is duplicate or late | The next period may already exist or activation may be delayed | Reuse the persisted period account and grant IDs; exact-lookup both, then activate only for the intended period |
| Reset workflow stops after some writes | The selected dimensions may be ledger-complete but not yet application-active | Keep admission gated and resume the same durable reset record; never replace its IDs or infer completion |
| Rolling post already has a restoration | A normal replenishment or reset adjustment already returned its capacity | Treat the source post as satisfied; never submit the mutually exclusive transfer |
TigerBeetle operation routes have no separate request idempotency key. Stable account and transfer IDs are the idempotency boundary. Persist exact payloads and their order before the first write.
A linked chain is all-or-none inside one create_transfers request. Normal unlinked neighbors can partially succeed. The gateway may report only a bounded number of conflicts, so reconcile every ID rather than assuming an omitted item succeeded.
Test scenarios
Run these tests with synthetic identities in an isolated non-production database. Explicitly post or void all pending test transfers; do not use natural expiry as the normal terminal path.
| Scenario | Expected result |
|---|---|
| Account provisioning | Nine accounts exist with three governed ledgers; customer, credit-sink, and rolling-sink accounts use flags 10 |
| One displayed credit | Granting 1,000,000 atomic units displays exactly 1.000000 credit under the example scale |
| Fixed-point rating | Maximum example is 1,200; final example is 720; no path converts through Number |
| Credit admission | Pending 1,200 lowers available credit without changing posted totals |
| Credit partial post | Posting 720 records the exact burn and releases 480 permanently |
| Credit zero usage | Explicit void releases all 1,200; no zero post is created |
| Different model | A new rate version changes reserve and actual burn while the user retains one common credit balance |
| Concurrent credit requests | Combined reservations cannot exceed the constrained customer credit balance |
| Dual-limit happy path | Weekly and rolling estimates commit together, then exact partial posts commit together |
| Weekly limit exhausted | Linked admission rejects both weekly and rolling estimates; provider is not called |
| Rolling limit exhausted | Linked admission rejects both estimates even when weekly capacity remains |
| Actual less than estimate | Partial posts deduct 2,400 and release the unused 1,600 from both reservations |
| Actual equals estimate | Full amounts post to both dimensions with no remainder |
| Actual exceeds estimate | Application blocks settlement and opens overage review; it does not fabricate extra capacity |
| Failed provider request | Definitive zero usage voids both weekly and rolling estimates atomically and creates no replenishment |
| Disconnect with continuing generation | Reservation stays open until provider reconciliation supplies final usage |
| Five-hour replenishment | Exact rolling usage returns once at reservation timestamp plus five hours |
| Early replenishment guard | Scheduler refuses to submit before the persisted due time |
| Late replenishment | Capacity stays conservative until the same stable replenishment succeeds |
| Scheduled weekly reset | New requests use a newly funded period account at the boundary; admitted requests remain pinned to the old week |
| Early weekly provisioning | The next account and grant may exist before the boundary, but routing does not activate early |
| Duplicate weekly reset | The same account and grant reconcile once; no second allowance is issued |
| Manual same-week reset | A new audited account generation receives one grant; old balances and history remain unchanged |
| Rolling-only reset | Selected posted usage returns once; the current weekly pointer and pending reservations remain unchanged |
| Weekly-only reset | The weekly pointer advances; rolling posted usage, due jobs, and pending reservations remain unchanged |
| Reset both limits | One reset saga reconciles the new weekly generation and every selected rolling adjustment before admission opens |
| Rolling reset with pending usage | Restored availability remains below the limit by the exact in-flight pending amount |
| Rolling reset versus scheduler race | One unique per-post restoration wins; the other worker exact-lookups and no-ops |
| Ambiguous reset write | Recovery reuses the persisted reset account and transfer IDs after exact lookup |
| Partial reset visibility | Admission remains paused and the reset enters reconciliation; replacement IDs are not allocated |
| Duplicate delivery | Stable IDs and exact lookup produce one reservation and one terminal chain |
| Same ID, changed field | Exact comparison quarantines the request; no replacement ID is generated |
| Ambiguous response | Lookup identifies exact commit or all-absent state before any identical retry |
| Partial linked chain | No partial commit is visible; any partial lookup becomes an incident |
| Shared pending accounting | Explicit post/void closes each reservation and the observed open-pending count returns to baseline |
| Plan change mid-request | Request settles against its pinned mode and policy version; new requests use the new plan |
| No PII | IDs, user-data, payload logs, and error telemetry contain only opaque references |
Production operations
- Use an approved Production or Enterprise topology for production AI usage. Treat Shared as a bounded integration environment and Dedicated Single Node as non-HA and non-production.
- Version the displayed credit scale, every model meter, rounding rule, weekly policy, rolling duration, provider mapping, and transfer-code registry. Never retroactively re-rate a posted request.
- Enforce maximum generation at the provider boundary so final usage cannot exceed the reserved amount. Include tool, image, audio, cache, reasoning, or fixed-request meters when the provider bills them.
- Persist provider usage before ledger settlement. Deduplicate usage events by provider and provider request ID, and quarantine conflicting duplicates.
- Monitor available credit, weekly and rolling pending debits, pending age, settlement lag, declines, provider/ledger mismatches, and exact-lookup incidents.
- Reconcile credit grants to purchases or promotions, credit burns to provider usage, weekly posts to the pinned period, rolling posts to exactly one replenishment, and customer percentages to the correct policy version.
- Reconcile every weekly period to one active account generation and one grant. Alert on early activation, missing grants, duplicate generations, and reset workflows that remain blocked.
- Sweep stuck in-flight reservations only after provider reconciliation. Natural expiry is a safety backstop, not proof of provider cancellation.
- Alert before pending-transfer and event quotas are exhausted. Dual-limit admission and settlement create more ledger events than simple posted metering.
- Keep the two-item dual-limit reservation and two-item settlement pairs intact. If the active events-per-request limit cannot admit either chain, change the topology or product design; do not split away atomicity.
- Monitor replenishment backlog and clock skew. Never submit a replenishment before the original reservation timestamp plus five hours; a late replenishment is safer than an early one.
- Restrict manual resets to authorized operators and require a case, reason, target, actor, timestamp, and immutable before/after evidence. Rate-limit the control and surface it in customer and operator audit history.
- Enforce one restoration mapping per rolling post across both normal
3020replenishments and early9000adjustments. A reset must never create an aggregate rolling grant while per-post replenishments remain armed. - Treat a late provider usage event, expired terminal record, duplicate mismatch, or over-reserve usage as a reconciliation incident with an owner and evidence trail.
- Use new stable refund or adjustment transfers for approved corrections. Never delete, mutate, or reuse a historical transfer ID.
- Protect database-scoped API keys in server-side secret storage, rotate them, and separate service keys from human CLI OAuth sessions.
- Keep prompts, completions, customer identifiers, raw provider payloads, secrets, and PII out of TigerBeetle fields and general logs.
Related documentation
- Metering for durable integer usage and invoice inputs without the two-mode AI admission workflow.
- Subscriptions for recurring plan entitlements, billing periods, credits, and adjustments.
- TigerBeetle operations for supported gateway operations and payload rules.
- Parix CLI TigerBeetle commands for current command options.
- API keys and authentication for credential scopes and database boundaries.
- Plans and limits for current topology, pending-transfer, timeout, and request quotas.