Docs/Ledger APIv0.0.1-preview

Accounts

Creating accounts, flags and limits, funding through control accounts, and the account lifecycle.

On this page
  1. Creating an account
  2. Limits
  3. Funding and control accounts
  4. Lifecycle
  5. Reading accounts

Accounts are created explicitly, behave according to flags declared at creation, and are never deleted — the account space is part of the audit record.

Creating an account

An account create carries: a 128-bit id (caller-chosen), ledger_id, currency (ISO 4217 numeric), tier, flags, optional limit_low / limit_high, and an optional participant_id for grouping accounts under an owning participant.

Every id here is a number: account ids are 128-bit integers, ledger_id and source_id are 32-bit integers — in the SDKs you pass integers, never strings. An account is named by its full key, (ledger_id, currency, id): every read or update of an account names all three, and a key that names no account answers ERR_NOT_FOUND. In this release the id is also still unique across the engine — creating id 100 under a second ledger or currency, or re-creating it with different attributes, answers ERR_ACCOUNT_CONFLICT (44) — so partition your id space by ledger or tenant up front. An identical retry of the original create is OK_DUPLICATE, even after later limit updates. The one string identifier in the system is a transfer’s txn_id, your business reference. (Over the REST gateway, account ids travel as decimal strings like "1001" — a JSON-safety convention for the full 128-bit space, not a naming scheme.)

from tuladb.models import AccountCreate

c.create_account(AccountCreate(
    id=1001,
    ledger_id=1,
    currency=840,
    flags=1,                      # DEBIT_NORMAL
    limit_low=0,                  # never below zero
    limit_high=1_000_000_000_000,
))

The wire protocol has an account batch operation — up to 1,024 creates per request, with a per-item status for each — but in this release every SDK and tulactl sends one account per call, and no REST gateway exposes a bulk create. Provision accounts in a loop (or pipeline the calls) for now.

Flags

Flag Bit Behaviour
DEBIT_NORMAL 1 Normal balance side is debit (a label — see below)
CREDIT_NORMAL 2 Normal balance side is credit (a label — see below)
CONTROL 4 Control account for funding/defunding (immutable)
SETTLEMENT 8 Settlement-tier account (immutable)
ALLOW_NEGATIVE 16 Posted balance may go negative
CLOSED 32 Terminal — no further activity
NO_DEBIT 64 Refuses debits
NO_CREDIT 128 Refuses credits
INACTIVE 256 Temporarily refuses activity (reversible)

The common default is 3 (DEBIT_NORMAL | CREDIT_NORMAL). DEBIT_NORMAL and CREDIT_NORMAL are labels for your own reporting: the engine stores them but no balance, limit or sign rule reads them — limits and ALLOW_NEGATIVE decide what an account may do.

Limits

limit_low and limit_high bound the account’s balance, enforced by the engine on every mutation. A transfer that would cross a limit is declined with ERR_LIMIT — and the decline is recorded like any other rejection.

Limits can be updated after creation — one side per request, so an audit trail shows exactly which bound changed:

from tuladb.models import AccountKey

key = AccountKey(ledger_id=1, currency=840, id=1001)
c.update_account_limit_low(key, 0)
c.update_account_limit_high(key, 5_000_000)

participant_id is create-only and never mutable — it’s identity, not configuration.

Funding and control accounts

Value enters and leaves a ledger through control accounts. Seeding a customer account is itself a balanced transfer from a control account, so double-entry holds even at the boundary:

tulactl create-account --id 1000 --ledger 1 --currency 840 --control --credit-normal
tulactl seed --control 1000 --id 1 --amount 100000 --ledger 1 --currency 840 --source 1001 --txn demo-seed-1

There is no “set balance” operation anywhere in the API — a balance is only ever the sum of its postings.

Lifecycle

Deactivate an account to refuse new activity while keeping history queryable, and reactivate it later:

c.set_account_active(key, False)    # transfers now decline with ERR_DEBIT_ACCOUNT_INACTIVE (26)
                                    #   or ERR_CREDIT_ACCOUNT_INACTIVE (27)
c.set_account_active(key, True)

CLOSED is set only at creation (tulactl create-account --closed, or the flag on AccountCreate); there is no call that closes an existing account, and set_account_active on a closed account answers ERR_CONFIG. To retire an account in use, deactivate it.

Creation and limit changes are kept as an ordered account metadata history, queryable per account: each event carries its timestamp and the old and new limits. Activation changes are not in it, and no event records who made the change — for that, use the console’s audit log, or the engine’s request audit log.

Reading accounts

balance = q.get_balance(key)              # six-field Balance snapshot
rows = q.list_account_balances(1, 1001)   # every currency id 1001 holds in ledger 1
total, accounts = q.list_accounts(offset=0, limit=256)   # total = every account, not just this page
events = q.get_account_history(key)       # metadata event trail

Account-addressed calls take the key, never a bare id (the Python SDK raises TypeError on an id-only call rather than guess which account it meant).

Balance reads reflect applied state on the serving node. q here is a query-port client, where a balance is a local read and not linearizable unless cluster_linearizable_query_reads is enabled; when the answer gates a spend, read it on the client port (c), which is linearizable by default. Listing and search go through derived indexes — see Queries for the consistency contract.