Symbols
Units
All monetary fields are integer drops (1 XRP = drops), carried as decimal strings of up to 19 significant digits. All arithmetic usesdecimal.js at precision 40. Number() and parseFloat are never applied to a monetary value: 19 significant digits do not survive an IEEE-754 double, and BigInt("1e17") throws on the scientific notation rippled sometimes emits. The helper num() in src/num.mjs is the single place where “field absent” is coalesced to zero, because rippled omits any field equal to its type default.
Rate scale
and are expressed in units of . A stated value of10000 is therefore — not and not .
1. Net Asset Value, Two Ways
Orma computes and reports NAV two ways on every call — not to be thorough, but because the two numbers differ and one of them is silently wrong for any vault carrying an unrealised loss. The naive reading, which is what a metadata-diffing indexer computes: The correct reading, net of the loss the manager has already recognised: Both are defined as zero when . Divergence is reported in basis points: is computed from the full-precision
Decimal values, not from the rounded six-decimal strings. The integer conversion is a round, not a truncation: bps() in src/num.mjs calls toFixed(0), and Decimal.prototype.toFixed rounds half-up. On Calder Structured Credit III the exact value is bp, so the API reports 1961. A floor would report 1960. If you reimplement this and get 1960, that is the reason.Reader.nav() in src/poll.mjs:
Why the naive figure is not merely lazy
rippled omits fromPreviousFields any field whose previous value was the type default. On the first impairment of a healthy vault, moves from , so PreviousFields is emitted empty. Under cash-basis accounting (LEVersion = 1) an impairment changes nothing else on the Vault object. An indexer that diffs transaction metadata therefore observes no change at all on the vault it is watching.
Captured from transaction 075FE6D2E0F29919AF477A2A8F581A680805A006138BB967D8434611E49229C3 (tesSUCCESS, Meridian Trade Finance I):
2. First-Loss Cover
Cover required and cover shortfall
Max liquidatable now
The amount of first-loss capital that can actually be liquidated in a single default event:Stranded cover fraction
Cover that has been posted but cannot currently be liquidated — it is frozen above the liquidation ceiling: Defined as when . The derivation insrc/score.mjs:
3. Cover Consumed on a Default
When a broker callsLoanManage with the DEFAULT flag, the ledger liquidates:
Three properties of this formula, each of which matters:
- The double product. Both rates are units, so both are divided by (see above).
- The result is ceilinged, not floored and not rounded.
- The base is the broker’s total book , not the principal of the exposure that defaulted. The same loan defaulting inside a larger book liquidates more cover. is decremented as each default lands, so each successive default consumes less.
coverForOrder() in src/history.mjs, which mirrors the ledger step by step:
LoanManage.cpp:146–169, with decremented at line 248.
4. The Ordering Result
Because is decremented as each default lands, each default shrinks the base for the next one. The total cover paid across a fixed set of losses therefore depends on the order they are declared in. Write and let be the book at the moment of the first default. Declaring defaults in the order consumes, while cover remains available and each is large enough that the never binds: Only the sum depends on . The coefficient is decreasing in , so by the rearrangement inequality the sum is maximised, and therefore minimised, when is decreasing. Hence:Total first-loss capital consumed is minimised by declaring the largest exposure first, and maximised by declaring the smallest first.The party who chooses is the
LoanBroker owner, who is necessarily the vault owner (LoanBrokerSet.cpp:109), and is the party whose capital is consumed. The difference lands on the unit holders.
The closed form drops the ceiling and the clamps, which is why it is stated with a proviso. Use the closed form to understand the result; use the simulation in
coverForOrder() to compute it.Measured on Devnet
Kestrel Bridge Financing II: , , :
A 40% difference in what the first-loss capital absorbed, on identical losses. Three independent agreements: the iterative simulation in
coverForOrder(), the closed form above, and the ledger itself.
Fairness score
Observed consumption placed between the two extremes: is investor-worst (the sequence that minimised cover consumed, leaving the maximum loss for depositors). is investor-best (the sequence that maximised cover consumed on the broker’s behalf). is defined as when , meaning the ordering could not have changed anything.Read the JSON field names carefully before comparing. In
GET /api/vaults/{id}/broker-history the score is the field fairness. worstPossible is the smallest cover number (largest-first order, worst for investors), and bestPossible is the largest. The names are from the investors’ point of view, not the arithmetic’s.costToDepositors is depositor money. Reconstructing it required no privileged access.
5. Capital Destruction
Every other factor measures current exposure — and a realised loss leaves none behind. When a write-off settles, the asset is removed fromAssetsTotal, the provision is released, and the book reads clean. Without an explicit memory term, a vault that just wrote off four-fifths of its portfolio would score identically to one that never lost a penny.
Par is for a closed-ended vault: subscription closes before any lending, so is capital subscribed measured in asset units, and later subscribers join at the same unit value with no profit or loss yet accrued. This identity does not hold for an open-ended vault and would need rethinking in that case.
6. Composite Grade by Ordinal Notching
Orma grades are ordinal and rule-based, never a weighted sum. A weighted average lets a strong factor compensate for a broken one — and that is not the question an investor is asking.The ladder
Let , , ordered best to worst, and the position of grade :Dimension bands
Each dimension maps its raw value to an initial band grade before any notching. Bands are evaluated top to bottom; the first threshold the value reaches wins, and D if none does.LIQUIDITY — AssetsAvailable / AssetsTotal
LIQUIDITY — AssetsAvailable / AssetsTotal
Defined as when .
COVER — first-loss adequacy
COVER — first-loss adequacy
The denominator takes the larger of the recognised loss and the distressed principal. Dividing by alone would grade a vault concealing a large overdue exposure as perfectly covered. Concealment must never improve a score.
Defined as when the denominator is zero (no exposure to absorb).
CONCENT — concentration
CONCENT — concentration
Graded on , where .
Note: the API reports
concentration raw (higher is worse) with worseIsHigher: true. The grading is done internally on the complement.RECOG — recognition lag
RECOG — recognition lag
A direct cut on seconds, not a ratio:
recogLagSeconds is the maximum seconds any loan with status overdue or defaultable has been overdue without being declared, floored at zero.DEADLINE — redemption cliff
DEADLINE — redemption cliff
where is the total contractual repayment of performing loans (status
current or due_soon) and .Graded on :rippled refuses any
LoanSet whose maturity overruns the vault’s RedemptionDate, so every performing loan is contractually repaid before redemption. A projected shortfall is therefore exactly the non-performing book net of recoverable cover. Being fully lent is not a distress signal.Notch rules
The composite anchors on DEADLINE — for a fixed-term facility the headline question is whether claims can be met at redemption — then notches from there:
The two recognition-lag rules are exclusive (the larger wins). The realised-destruction bands are also exclusive. Every notch applied is emitted in
notchTrace on the API response, with from, rule, delta, and to. That trace is the answer to “how did you choose your weights” — there are no weights, there are notches, and each one is named.
Worked example — Calder Structured Credit III
, , , , , , , one impaired loan of , phase: Investment.
Notching: anchor AAA. because , no capital-destruction notch. Concentration , → AA+. Adequacy , → AA. Numeric: .
7. Conduct Score
The conduct score is a separate ordinal assessment of the manager, not of the book. It starts at 100 and is clamped to :
Mapped to letter grades: , , , , below.
Finding codes
ORDERING_SELF_SERVING
ORDERING_SELF_SERVING
Declared losses in an order close to the one that minimises the broker’s own first-loss contribution (fairness score ). The choice cost investors the difference between
actualCoverPaid and bestPossible.DEFAULT_WITHOUT_IMPAIRMENT
DEFAULT_WITHOUT_IMPAIRMENT
An exposure was written off with no prior impairment — the loss went straight from invisible to realised, with no window for anyone to react. Penalty per occurrence.
COVER_WITHDRAWN
COVER_WITHDRAWN
A withdrawal of first-loss capital is on record. Cover taken out ahead of a deteriorating book is the pattern that preceded losses at Maple Finance in 2022. Penalty per withdrawal.
Devnet prunes account history to roughly 29 days, so the conduct score covers a window, not a lifetime. The API says so in a
caveat field on the reputation block of GET /api/vaults/{id}/broker-history.8. Pledge Valuation
For units pledged as collateral, Orma reports both readings so a lender can see what a naive computation would have told them: A lender’s own haircut applies to the honest value, not the reported one: Measured. units against : reported , held , overstatement . Two functions compute this, with deliberately different field names because they answer different questions:- valuePledge (collateral.mjs)
9. Oracle Encoding (XLS-47)
Orma publishes sixPriceData entries per vault as a native XLS-47 Price Oracle — one per dimension. The base asset is derived from the vault ID; the quote asset is the dimension code.
Everything except NAV is clamped to . NAV is a price and may exceed 1.
A stored value at scale decodes to .
AssetPrice is a UInt64 serialised as hexadecimal, so writing "100" means 256, not one hundred:
Decoded from a live oracle object
Oracle object03C7B0E151129510F11FF92CCE8FBB3E7AD3C55B25EDA6EB414324E3E995078A, document id 3, for Calder Structured Credit III:
Four publication invariants
Each of these corrupts data silently rather than erroring:OracleSet is not a merge
OracleSet is not a merge
A pair already on the object but omitted from the transaction is kept with its
AssetPrice and Scale stripped. Publishing one dimension blanks the other five. The publisher always resends all six. There is also no way to remove a pair, so reusing a document ID for a different set of assets accumulates stale pairs until OracleSet returns tecARRAY_TOO_LARGE. The only recovery path is OracleDelete then recreate. Document IDs come from an explicit table, never from a hash of the vault ID, because the ID is a UInt32 scoped to (account, ID) and a collision would silently overwrite another vault’s scores.LastUpdateTime must strictly increase
LastUpdateTime must strictly increase
Equal or lower gives
tecINVALID_UPDATE_TIME, burning a fee and a sequence number. The publisher uses ledger close time clamped to Date.now() and skips a tick rather than post ahead of the clock.LastUpdateTime in the future bricks the object
LastUpdateTime in the future bricks the object
A
LastUpdateTime in the future bricks the object until wall clock catches up. Never add a safety margin.AssetPrice is hex, not decimal
AssetPrice is hex, not decimal
AssetPrice is a UInt64 serialised as hexadecimal. Writing the decimal string "803922" would be interpreted as hex , giving a NAV nine orders of magnitude too large. Always encode with BigInt(n).toString(16).toUpperCase().LastUpdateTime is UNIX epoch, not Ripple epoch.Checking This Yourself
Start from the valuation document, which names the ledger state it was computed from precisely so you do not have to trust it:AssetsTotal and LossUnrealized should equal provenance.assetsTotal and provenance.lossUnrealized as of the ledgerIndex on the response envelope. If LossUnrealized is absent, that means zero — rippled omits any field equal to its type default, which is the same rule that makes the first impairment invisible to a diffing indexer.
1
Verify the grade
Every notch is in
notchTrace on GET /api/vaults/{id}, with the grade before and after. Walk it against the band tables in Section 6 and the notch rules table.2
Verify the cover arithmetic
GET /api/vaults/{id}/broker-history lists every default with debtBefore and coverConsumed. Check each against using the rates on the broker, then check the total against coverForOrder() run over the same principals in both extreme orders.3
Verify the oracle
Fetch the object by its index on the Devnet explorer and decode
AssetPrice yourself with BigInt('0x' + raw) divided by . It should agree with the API to the last digit, because both come from the same snapshot.Everything on this page runs on Devnet at
wss://s.devnet.rippletest.net:51233 (network ID 2), because XLS-65 vaults and XLS-66 lending are not yet on Mainnet. The oracle publication via XLS-47 is the one component with a production path today.