Ledger
The ledger (@corridor/ledger) is the system of record. The chain and the anchors are evidence; the ledger is what Corridor says happened, and reconciliation proves the two agree.
Accounts
Section titled “Accounts”An account is identified by its type and owner, scoped by corridor, asset and venue:
{type}:{ownerType}:{ownerId}:{corridor | *}:{asset}:{venue}
liability:customer:eu-payco:EUR-KES:USDC:stellarasset:vault:treasury:*:USDC:stellarclearing:clearing:usd-peg:*:USDC:solanaexpense:expense:gas:*:XLM:stellar| Type | Natural balance | Used for |
|---|---|---|
asset |
Debit | Vaults: real funds Corridor controls on a venue (the Stellar treasury, the spoke egress and ingress vaults) |
liability |
Credit | What Corridor owes customers: available balance (customer) and funds held for an in-flight payout (customer_hold) |
clearing |
Debit | Conversions between assets and between venues. Each pair nets to zero over a flow |
revenue |
Credit | Fees earned |
expense |
Debit | Costs borne: XLM network fees, sponsored reserves |
equity |
Credit | Owner funding: fee float, Friendbot funding on testnet |
Named helpers (Accounts.customer, .customerHold, .vault, .clearing, .revenue, .expense, .equity) open accounts on first use, so legs never build account ids by hand.
Precision
Section titled “Precision”Every amount is an integer count of minor units, and the digital assets are kept at 7 decimal places — Stellar’s own precision. Converting a Horizon decimal string into the ledger is formatting, never rounding, so a vault’s on-chain balance can equal the ledger exactly. Spokes with fewer decimals convert through rescale, which refuses any conversion that would drop value. See ADR 0012.
Invariants
Section titled “Invariants”The sum of debits equals the sum of credits for each asset in an entry. Checked before any write; an unbalanced entry never reaches the database.
Vaults, customer balances and holds are flagged no_overdraft. After applying postings, the natural balance of each such account is checked inside the same transaction, which rolls back on violation.
unique (external_venue, external_ref) on journal entries. A Stellar payment, an anchor order or a Solana signature can be booked once; a second attempt returns the original entry as a duplicate.
Database triggers reject UPDATE and DELETE on the journal and postings. Mistakes are corrected with reversing entries, so the history is never rewritten.
checkInvariants() re-verifies globally — every entry balances per asset, and the balance projection equals a full recompute from postings — and runs in tests, at the end of pnpm stellar:payout, and on demand.
Schema
Section titled “Schema”accounts (id, type, owner_type, owner_id, corridor, asset, venue, no_overdraft)journal_entries (id, kind, saga_id, reference, external_venue, external_ref, metadata, unique (external_venue, external_ref))postings (entry_id, account_id, asset, direction 'D'|'C', amount numeric(78,0) > 0)balances (account_id, asset, debits, credits) -- projection, same transactionAmounts are numeric(78,0) integer minor units, wide enough for any 256-bit on-chain amount. Balance rows are locked in a stable order before posting, so concurrent writers cannot deadlock.
Posting
Section titled “Posting”await ledger.post({ kind: "partner.payout", reference: d.reference, // the Corridor Reference sagaId: ctx.sagaId, external: { venue: "partner_fiat", ref: `${anchor}:${orderId}` }, lines: [ { account: hold, direction: "D", money: money("USDC", 70_000_000n) }, { account: treasury, direction: "C", money: money("USDC", 70_000_000n) }, ],});Seven USDC at the hub’s 7 decimal places: the customer’s hold is released and the treasury vault is credited out, keyed by the anchor’s order so it can only be booked once.
An entry can carry several assets, each balanced in its own: a conversion from EURC into USDC debits and credits both sides through a clearing pair, which nets to zero across the flow.
Reconciliation needs to know which side moved
Section titled “Reconciliation needs to know which side moved”entryTotal(..., { excludeLiabilities: true })Now that the hub and its spokes settle in the same asset, a payout debits the customer’s hold in USDC and moves USDC on the chain — and only the second is what an observer saw. excludeLiabilities gives the movement across Corridor’s own positions, which is the number to compare with the chain.
Storage
Section titled “Storage”Locally and in tests the ledger runs on PGlite, Postgres compiled to WebAssembly, so a developer needs no database server. Production uses managed Postgres with the same schema and triggers. Both satisfy the same small Db interface (query, exec, transaction).