Ledger (double-entry)
Every movement of funds is a set of balanced entries, and balances are computed from the ledger. Available and reserved, historical balances, idempotency, and immutability.
In V3 Custody, money isn't a number that gets overwritten; it's a ledger. Every movement of funds is recorded with double-entry accounting, and balances are computed from those records. So answering "how much and why" takes a query, not an investigation.
Entries that always balance
Each ledger record (entry) consists of postings, and their sum is zero. One side touches your address's account, and the other touches the counterpart of the movement (the on-chain side or an adjustment). A zero sum guarantees that funds never appear or disappear; they only move.
Schematically, an outgoing transfer of 100 USDT goes through two entries. First, a reserve within your address:
| Account | Posting |
|---|---|
your address · available | −100 |
your address · reserved | +100 |
| Total | 0 |
Then, execution after signing:
| Account | Posting |
|---|---|
your address · reserved | −100 |
| counterpart (on-chain) | +100 |
| Total | 0 |
The ledger is read-only and immutable:
GET /api/v1/vaults/{code}/ledger/entriesreturns immutable entries with their postings (postings sum to 0). Sorted bypostedAt(newest first), with keyset pagination and thefrom/to/tokenfilters. A regular member sees only entries that touch their own addresses; an administrator sees the entire Vault.
Balances are computed, not stored
A balance is a rollup of the ledger, not a separately stored number. It's updated atomically with the postings, so it can never drift from them. Every balance is split into two states:
| State | What it is | When it changes |
|---|---|---|
available | free to spend | ↓ when a transfer is created; ↑ when a reserve is returned or funds arrive |
reserved | locked by pending transfers, but still yours | ↑ when a transfer is created; ↓ on execution or return |
| "owned" | available + reserved | the sum of the two states |
GET /api/v1/vaults/{code}/balancesreturns balances for a Vault, a user, or an address, with thesummary,addressId, andtokenparameters. The response contains per-token totals (totals).
Reserved: funds "in transit," but still yours
When a transfer is created, the amount is first reserved, even before the transaction is sent to the network. The reserve protects against double spending: the same funds can't be sent twice.
What happens to the reserve depends on the Policy Engine. If the transfer is allowed and confirmed, the reserve is debited. If it's queued for approval, the amount waits in reserved. If it's cancelled or rejected, the amount returns to available. The funds remain yours the entire time.
How to read a balance
- Current:
GET /api/v1/vaults/{code}/balances(availableandreservedright now). - As of a date:
GET /api/v1/vaults/{code}/balances/as-of?at=...(a past position, for statements and reconciliation). - Summary or per address:
summary=true(per-token totals only) oraddressId(a single address). - What you can spend: go by
available;available + reservedis the total that's yours.
Balances at any point in the past
Because the ledger is immutable, a position can be reconstructed for any date from the postings recorded before that point:
GET /api/v1/vaults/{code}/balances/as-of?at=...returns the balance as ofat, calculated from postings withpostedAt <= at. The past never changes retroactively. The "owned" balance =available + reserved; a regular member sees only their own addresses.
This is exactly what you need for end-of-period statements and reconciliation: the answer to "how much was there on March 31" is always the same.
Idempotency: one request, one payment
Transfers accept an idempotencyKey. Retrying a request with the same key returns the operation that was already created instead of creating a new one. That makes retries after network failures safe, with no risk of a double payment. For details, see Idempotency.
Nothing is rewritten, only appended
Ledger entries are never edited or deleted. A mistake is corrected with a new operation linked to the original, so the full history is preserved:
| Relation type | Meaning |
|---|---|
refund | a return of funds (partial refunds use relatedAmountRaw) |
reversal | a reversal or cancellation of the original operation |
correction | a fix for an error in the original operation |
replacement | replacement of the operation with a new one |
POST /api/v1/vaults/{code}/operation-relationscreates an explicit link between operations. An opposite payment is not automatically treated as a refund; the link must be set explicitly. The exact linking rules are in the API Reference.
Each operation also has a status history:
GET /api/v1/vaults/{code}/operations/{type}/{id}/status-historyreturns the timeline of status transitions.
The operations feed and the ledger: two different views
The ledger (postings) is the accounting truth. For humans, though, the operations feed of business events is more convenient:
GET /api/v1/vaults/{code}/historyreturns your activity in the Vault as a single feed (your transfers and deposits to your addresses), sorted by(createdAt, id)descending, with keyset pagination. Filters:direction=in|out,from/to, and status (for example, stuck transfers).
Postings answer "how is this reflected in the books," while the feed answers "what happened."
Amounts in the ledger and balances are stored in the token's smallest units (raw) and displayed in decimal form using the network's decimals. For details, see Amounts and precision.