Skip to main content
Orma is two processes: a Node reader that polls XLS-65 vaults on XRPL Devnet every four seconds, scores them, and serves a frozen JSON contract on port 8787 — and a Vite frontend that consumes it. Neither requires a database. The reader holds its state in memory and re-reads the full ledger object every tick. This page gets both running against four facilities that already exist on Devnet.
Everything here runs on XRPL Devnet (wss://s.devnet.rippletest.net:51233, network id 2). XLS-65 vaults and XLS-66 lending are not on Mainnet, so the reader, scorer, and gate cannot be pointed at production assets today. Devnet is not a convenience — it is the only network where XLS-65 and XLS-66 exist.

Prerequisites

Two runtime dependencies, both pinned exactly: xrpl at 5.2.0 and decimal.js at 10.6.0.
Do not run npm install or pnpm install in the repository root. node_modules/ ships installed and verified against live Devnet. Re-resolving it is the one reliable way to lose a working toolchain.The frontend is the exception: app/ is a separate dependency tree and pnpm install there is expected and safe.

Why the xrpl pin is exact

xrpl@5.2.0 fixes two blocking issues for the lending stack:
  • VaultCreate with VaultKind, SubscriptionDate, and RedemptionDate now encodes natively. Before 5.2.0, validate() rejected those fields, making the very first transaction of the lending workflow unreachable through the model layer.
  • signLoanSetByCounterparty emits the correct CPT\0 (0x43505400) prefix, eliminating the manual prefix swap that previous code needed.
Two sharp edges survive on 5.2.0 and are handled in the shipped code:
  • The LoanSet fee must be doubled by hand. autofill warns about the second signature but does not pay for it. Every baker does tx.Fee = String(Number(tx.Fee) * 2).
  • OracleSet is signed raw. validate() caps Scale at 0–10 while rippled accepts 0–20, and demands Scale whenever AssetPrice is present while rippled forbids an explicit Scale: 0. Together these make some legal oracles unreachable through the model layer.
xrpl-py is still on 5.1.0 and did not receive either fix. Keep Python out of the signing path.

Steps

1

Verify the toolchain

Run this first, every session, before trusting anything else. It drives the entire lending chain against live Devnet with no workarounds: VaultCreateVaultDepositLoanBrokerSetLoanBrokerCoverDepositLoanSet with a native counterparty signature.
A successful run looks like this (elapsed times vary with Devnet close times):
The middle line is autofill’s own warning about the second signature — it prints the warning and then does not double the fee, which is why every baker doubles it manually. The loan ID changes every run because the script builds a fresh vault each time.If this fails, stop. Nothing downstream will work and the failure is almost always the toolchain rather than your code.
2

Start the reader

Run from the repository root. On startup, the entrypoint reads every demo/*.json, keeps any file carrying a 64-hex vaultId, and serves all four facilities automatically.
You should see:
publishing: false is correct — oracle publication is off until you supply a seed (see the optional step below). Logs go to stderr as JSON lines by default; set LOG_PRETTY=1 for human-readable output.The four facilities the reader discovers:
demo/ and .env are resolved as relative paths. Run the reader from the repository root. From anywhere else it finds no baked facility and exits with a usage message.
You can override the auto-discovered facilities with explicit arguments or an environment variable:
3

Call the API

With the reader running, verify the full chain is live with four requests.Check the reader is up and has completed its first poll:
ok stays false until the first poll completes — this is intentional, not a bug.Read all four grades, sorted worst first:
Notice that Kestrel — the most damaged facility — sorts first because Orma sorts by gradeNumeric ascending, not by NAV divergence. Kestrel shows zero divergence because its manager never impaired the loans before defaulting them; the losses were realised directly, releasing LossUnrealized back to zero. The score’s memory term (realised capital destruction, ρ=79%\rho = 79\%) is what catches it.Value a pledge from the share token ID alone:
Amounts are in drops (1 XRP = 1,000,000 drops). A lender valuing this pledge naively is over by 0.196078 XRP on every XRP of stated value.Check the gate on Thorne:
domainOwner is the vault owner, not Orma. They named our issuer unilaterally — we signed nothing and cannot refuse to be cited. See Gate.
4

Start the frontend (optional)

The frontend is a separate package. Run it from the app/ subdirectory.
Vite serves on http://localhost:5173 and polls the reader at http://localhost:8787 by default. Set VITE_API_BASE if your reader is on a different port. Three other optional variables are available: VITE_XRPL_NETWORK, VITE_XAMAN_API_KEY, and VITE_DOCS_URL.
Everything under VITE_ is inlined into the bundle at build time and shipped to every visitor. Never put secrets there. If you serve the frontend over HTTPS but point VITE_API_BASE at an http:// address, the browser will block the request as mixed content. The app detects this and caps its polling interval at 30 seconds rather than retrying at full rate against an address that will never answer.
5

Enable oracle publishing (optional)

Reading correctly is the product. Publishing is distribution, and a missing seed must never stop the reader from starting — so it is off by default.Create .env in the repository root:
The entrypoint parses .env itself with no external dependency. An existing shell variable always wins over the file. With a seed set, the reader publishes six PriceData entries per vault into a native XLS-47 Oracle object — one entry per dimension: NAV, HDL, LIQ, COV, CNC, DDL. It publishes on material change (held NAV or grade moved) plus a 60-second heartbeat.
Devnet faucet seeds are worthless but they are still private keys. .env and /.demo-keys.json are gitignored. A pre-commit hook checks every staged file for XRPL family seeds (s… or sEd…) and rejects the commit if it finds one. Do not weaken the hook — move the seed.
You can also set PUBLISH=faucet to fund a throwaway publisher automatically. Every restart will publish under a new account though, so the oracle objects from previous sessions are orphaned. Use a real seed if you want a continuous series.

Environment variables

All variables are optional.

What this does not do

Orma runs on Devnet, against rippled 3.4.0-rc5, because XLS-65 and XLS-66 are not on Mainnet. When those amendments activate on Mainnet, the reader needs only a different XRPL_WS and nothing else — but that is a claim about the code, not a current deployment. Grades are advisory. Every valuation response includes assessment.advisory: true. The only place an Orma opinion has force is the permissioned domain, and there the force comes from the ledger refusing a deposit — not from Orma.