Skip to main content
This page is the complete method reference — every formula Orma uses, the ledger fields it reads, and the source file that implements it. Use it to verify results or build your own implementation. Nothing here requires privileged access: every input is a public ledger field, and every figure the API serves names the ledger state it was computed from so you can recompute it yourself.

Symbols

Units

All monetary fields are integer drops (1 XRP = 10610^6 drops), carried as decimal strings of up to 19 significant digits. All arithmetic uses decimal.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

cminc_{\min} and cliqc_{\text{liq}} are expressed in units of 10510^{-5}. A stated value of 10000 is therefore 0.100.10 — not 1000010000 and not 100100.

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: NAVnaive=AS\text{NAV}_{\text{naive}} = \frac{A}{S} The correct reading, net of the loss the manager has already recognised: NAVheld=ALS\text{NAV}_{\text{held}} = \frac{A - L}{S} Both are defined as zero when S=0S = 0. Divergence is reported in basis points: δ=round ⁣(104NAVnaiveNAVheldNAVnaive)\delta = \operatorname{round}\!\left(10^{4} \cdot \frac{\text{NAV}_{\text{naive}} - \text{NAV}_{\text{held}}}{\text{NAV}_{\text{naive}}}\right)
δ\delta 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 1960.781960.78\ldots bp, so the API reports 1961. A floor would report 1960. If you reimplement this and get 1960, that is the reason.
The whole computation is four lines of Reader.nav() in src/poll.mjs:
Measured on Devnet. Calder Structured Credit III, A=51,000,000A = 51{,}000{,}000 drops, L=10,000,000L = 10{,}000{,}000, S=51,000,000S = 51{,}000{,}000: NAVnaive=1.000000\text{NAV}_{\text{naive}} = 1.000000, NAVheld=0.803922\text{NAV}_{\text{held}} = 0.803922, δ=1961\delta = 1961 bp.

Why the naive figure is not merely lazy

rippled omits from PreviousFields any field whose previous value was the type default. On the first impairment of a healthy vault, LL moves from 00, 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):
This is why Orma polls full object state every 4 seconds and never diffs PreviousFields. A diffing reader is not slightly late here — it is silent, until some later transaction happens to touch the same field for another reason. Reporting both NAV readings, with the gap named, lets a consumer see what a naive computation would have told them and decide for themselves.

2. First-Loss Cover

Cover required and cover shortfall

cover_required=Dcmin\text{cover\_required} = \lceil D \cdot c_{\min} \rceil cover_shortfall=max ⁣(0,  cover_requiredC)\text{cover\_shortfall} = \max\!\left(0,\; \text{cover\_required} - C\right)

Max liquidatable now

The amount of first-loss capital that can actually be liquidated in a single default event: max_liquidatable=Dcmincliq\text{max\_liquidatable} = \lceil D \cdot c_{\min} \cdot c_{\text{liq}} \rceil
Both rates are 10510^{-5} units, so both must be divided by 10510^5. At cmin=cliq=10000c_{\min} = c_{\text{liq}} = 10000 the coefficient is 0.10×0.10=0.010.10 \times 0.10 = 0.01. Dividing only once makes the answer 100 times too large.

Stranded cover fraction

Cover that has been posted but cannot currently be liquidated — it is frozen above the liquidation ceiling: stranded_cover_fraction=max ⁣(0,  Cmax_liquidatable)C\text{stranded\_cover\_fraction} = \frac{\max\!\left(0,\; C - \text{max\_liquidatable}\right)}{C} Defined as 00 when C=0C = 0. The derivation in src/score.mjs:

3. Cover Consumed on a Default

When a broker calls LoanManage with the DEFAULT flag, the ledger liquidates: T(p)=min ⁣(Dcmincliq,  p,  C)T(p) = \min\!\Big(\big\lceil D \cdot c_{\min} \cdot c_{\text{liq}} \big\rceil,\; p,\; C\Big) Three properties of this formula, each of which matters:
  1. The double product. Both rates are 10510^{-5} units, so both are divided by 10510^5 (see above).
  2. The result is ceilinged, not floored and not rounded.
  3. The base is the broker’s total book DD, not the principal pp of the exposure that defaulted. The same loan defaulting inside a larger book liquidates more cover. DD is decremented as each default lands, so each successive default consumes less.
Implemented in coverForOrder() in src/history.mjs, which mirrors the ledger step by step:
Verified against LoanManage.cpp:146–169, with DD decremented at line 248.

4. The Ordering Result

Because DD 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 c=cmincliqc = c_{\min} \cdot c_{\text{liq}} and let D0D_0 be the book at the moment of the first default. Declaring kk defaults in the order σ\sigma consumes, while cover remains available and each pip_i is large enough that the min\min never binds: T(σ,k)=c[kD0i=1k(ki)pσ(i)]T(\sigma, k) = c \left[ k D_0 - \sum_{i=1}^{k} (k - i)\, p_{\sigma(i)} \right] Only the sum depends on σ\sigma. The coefficient (ki)(k - i) is decreasing in ii, so by the rearrangement inequality the sum is maximised, and therefore TT minimised, when pσ(i)p_{\sigma(i)} 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 σ\sigma is the LoanBroker owner, who is necessarily the vault owner (LoanBrokerSet.cpp:109), and is the party whose capital CC is consumed. The difference lands on the unit holders.
The closed form drops the ceiling and the min\min 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: p={30 XRP, 10 XRP}p = \{30\ \text{XRP},\ 10\ \text{XRP}\}, D0=40 XRPD_0 = 40\ \text{XRP}, cmin=cliq=10000c_{\min} = c_{\text{liq}} = 10000: 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: ϕ=TobservedTminTmaxTmin,ϕ[0,1]\phi = \frac{T_{\text{observed}} - T_{\min}}{T_{\max} - T_{\min}}, \qquad \phi \in [0, 1] ϕ=0.0000\phi = 0.0000 is investor-worst (the sequence that minimised cover consumed, leaving the maximum loss for depositors). ϕ=1.0000\phi = 1.0000 is investor-best (the sequence that maximised cover consumed on the broker’s behalf). ϕ\phi is defined as 11 when Tmax=TminT_{\max} = T_{\min}, 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.
Reconstructed from ledger history alone for the Kestrel broker, which declared the 30 XRP loan first:
The 0.20 XRP of 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 from AssetsTotal, 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. ρ=max ⁣(0,  SAS)\rho = \max\!\left(0,\; \frac{S - A}{S}\right)
ρ\rho is measured against AA (total assets), not against ALA - L (assets net of provision). An unrealised loss is a provision against an asset the vault still holds and may recover. Counting it against ρ\rho would punish a manager for disclosing early — precisely the behaviour the rest of the system exists to reward. Only a write-off, which removes the asset from AssetsTotal outright, is permanent destruction.
Par is 1.01.0 for a closed-ended vault: subscription closes before any lending, so SS 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 ρ\rho would need rethinking in that case.
Measured. A vault that took 50 XRP, defaulted on 40, and holds 10.5: ρ=79%\rho = 79\%, Δ=14\Delta = -14 from AAA, composite B. Without this term the same vault scored AAA, identical to one that never lost a penny — and because the book sorts worst-first, the most damaged facility sorted last. That was observed on Devnet, not hypothesised.

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 L=[AAA,AA+,,D]\mathcal{L} = [\text{AAA}, \text{AA+}, \ldots, \text{D}], L=20|\mathcal{L}| = 20, ordered best to worst, and idx(g)\text{idx}(g) the position of grade gg: notch(g,Δ)=L[clamp(idx(g)Δ,  0,  19)]\text{notch}(g, \Delta) = \mathcal{L}\big[\operatorname{clamp}(\text{idx}(g) - \Delta,\; 0,\; 19)\big]
A negative Δ\Delta is a downgrade. The ladder runs best to worst, so a worse grade is a higher index — hence the minus sign.

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.
Defined as 11 when A=0A = 0.
adequacy=min ⁣(Dcmincliq,  C)max ⁣(L,  Pdistressed)\text{adequacy} = \frac{\min\!\big(\lceil D \cdot c_{\min} \cdot c_{\text{liq}} \rceil,\; C\big)}{\max\!\big(L,\; P_{\text{distressed}}\big)}The denominator takes the larger of the recognised loss and the distressed principal. Dividing by LL alone would grade a vault concealing a large overdue exposure as perfectly covered. Concealment must never improve a score.Defined as 11 when the denominator is zero (no exposure to absorb).
Graded on 1concentration1 - \text{concentration}, where concentration=maxi(pi)/D\text{concentration} = \max_i(p_i) / D.Note: the API reports concentration raw (higher is worse) with worseIsHigher: true. The grading is done internally on the complement.
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.
shortfall=max ⁣(0,  (AL)(Av+Rperforming+[Pdistressed>0]Crec))\text{shortfall} = \max\!\Big(0,\; (A - L) - \big(A_v + R_{\text{performing}} + [P_{\text{distressed}} > 0] \cdot C_{\text{rec}}\big)\Big)where RperformingR_{\text{performing}} is the total contractual repayment of performing loans (status current or due_soon) and Crec=min ⁣(Dcmincliq, C)C_{\text{rec}} = \min\!\big(\lceil D \cdot c_{\min} \cdot c_{\text{liq}} \rceil,\ C\big).Graded on 1shortfall%/1001 - \text{shortfall\%}/100: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

A=51,000,000A = 51{,}000{,}000, Av=41,000,000A_v = 41{,}000{,}000, L=10,000,000L = 10{,}000{,}000, S=51,000,000S = 51{,}000{,}000, D=10,000,000D = 10{,}000{,}000, C=5,000,000C = 5{,}000{,}000, cmin=cliq=10000c_{\min} = c_{\text{liq}} = 10000, one impaired loan of p=10,000,000p = 10{,}000{,}000, phase: Investment. Notching: anchor AAA. ρ=0\rho = 0 because ASA \ge S, no capital-destruction notch. Concentration 1.00.751.0 \ge 0.75, 1-1 → AA+. Adequacy 0.0100<0.050.0100 < 0.05, 1-1AA. Numeric: round(100(12/19))=89\operatorname{round}(100 \cdot (1 - 2/19)) = 89.

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 [0,100][0, 100]: Mapped to letter grades: A90A \ge 90, B75B \ge 75, C55C \ge 55, D35D \ge 35, EE below.
Measured on Devnet. The Kestrel broker scores 10035(2×20)=25100 - 35 - (2 \times 20) = 25, grade E: it declared the largest loan first (fairness 0.00000.0000), and neither loan was impaired before it was written off.

Finding codes

Declared losses in an order close to the one that minimises the broker’s own first-loss contribution (fairness score <0.25< 0.25). The choice cost investors the difference between actualCoverPaid and bestPossible.
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 20-20 per occurrence.
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 10-10 per withdrawal.
Reconstructing this history requires a join that is not obvious. LoanManage carries no LoanBrokerID, only LoanID, and an impairment does not touch the LoanBroker object at all. Matching on the broker node or a transaction field alone silently drops every impairment. Loan.FinalFields.LoanBrokerID in the same metadata closes the join at no extra request. Without that join, every default looks unsignalled — and a rule written to reward disclosure ends up penalising it.
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 uu units pledged as collateral, Orma reports both readings so a lender can see what a naive computation would have told them: Vreported=uNAVnaiveV_{\text{reported}} = u \cdot \text{NAV}_{\text{naive}} Vheld=uNAVheldV_{\text{held}} = u \cdot \text{NAV}_{\text{held}} overstatement=VreportedVheld\text{overstatement} = V_{\text{reported}} - V_{\text{held}} A lender’s own haircut hh applies to the honest value, not the reported one: Vlendable=Vheld(1h)V_{\text{lendable}} = V_{\text{held}} \cdot (1 - h)
A haircut absorbs volatility. It does not absorb a misstatement. A 20% haircut applied to a value that is 19.61% overstated leaves the lender roughly where they thought they were starting.
Measured. u=1,000,000u = 1{,}000{,}000 units against NAVheld=0.803922\text{NAV}_{\text{held}} = 0.803922: reported 1.000000 XRP1.000000\ \text{XRP}, held 0.803922 XRP0.803922\ \text{XRP}, overstatement 0.196078 XRP0.196078\ \text{XRP}. Two functions compute this, with deliberately different field names because they answer different questions:
Behind GET /api/mpt/{issuanceId}/nav?units=N. Takes a NAV document and a unit count; returns units, valueHeld, valueReported, overstatement. Returns an unpriced string rather than throwing when the document carries no unitValue, because a lender’s collateral tool must be able to say “I could not value this” rather than crash.

9. Oracle Encoding (XLS-47)

Orma publishes six PriceData 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 [0,1][0, 1]. NAV is a price and may exceed 1. A stored value vv at scale ss decodes to v10sv \cdot 10^{-s}. AssetPrice is a UInt64 serialised as hexadecimal, so writing "100" means 256, not one hundred:
The base asset uses the first 40 hex characters of the vault ID, with the leading byte forced non-zero:
Two traps motivate that last line: an all-zero currency code means XRP, and a code whose bytes 12–14 spell ASCII is rendered back by rippled as a three-character ticker symbol rather than a hex string.

Decoded from a live oracle object

Oracle object 03C7B0E151129510F11FF92CCE8FBB3E7AD3C55B25EDA6EB414324E3E995078A, document id 3, for Calder Structured Credit III:

Four publication invariants

Each of these corrupts data silently rather than erroring:
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.
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.
A LastUpdateTime in the future bricks the object until wall clock catches up. Never add a safety margin.
AssetPrice is a UInt64 serialised as hexadecimal. Writing the decimal string "803922" would be interpreted as hex 80392216=8,404,258803922_{16} = 8,404,258, giving a NAV nine orders of magnitude too large. Always encode with BigInt(n).toString(16).toUpperCase().
LastUpdateTime is UNIX epoch, not Ripple epoch.
XLS-47 is live on Mainnet today, unlike the lending amendments. get_aggregate_price computes a median and standard deviation across independent publishers inside the ledger — that is the point of publishing here rather than serving a number over HTTP. A second publisher who disagrees changes the aggregate without asking Orma’s permission. Orma reports aggregate: null honestly while it is the only publisher.

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:
Recompute both readings and the divergence from those three strings in decimal arithmetic, not in floats:
Then verify the provenance against the ledger directly — the step that makes the exercise non-circular:
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 Dcmincliq\lceil D \cdot c_{\min} \cdot c_{\text{liq}} \rceil 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 10Scale10^{\text{Scale}}. 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.