> ## Documentation Index
> Fetch the complete documentation index at: https://ormaprotocol.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart: Get the Orma Reader Running in Five Minutes

> Start the Orma reader against four live Devnet facilities, call your first API endpoint, and read a vault's credit score — all in under five minutes.

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.

<Note>
  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.
</Note>

## Prerequisites

| Requirement | Version           | Note                                                                                        |
| ----------- | ----------------- | ------------------------------------------------------------------------------------------- |
| Node        | 24.13.0           | `package.json` declares `engines.node >= 20`; all documented output was captured on 24.13.0 |
| npm         | bundled with Node | The repository root uses npm and `package-lock.json`                                        |
| pnpm        | any recent        | Only needed for `app/` — the frontend has its own `pnpm-lock.yaml`                          |
| Network     | outbound WSS      | `wss://s.devnet.rippletest.net:51233`, network id 2                                         |

Two runtime dependencies, both pinned exactly: `xrpl` at `5.2.0` and `decimal.js` at `10.6.0`.

<Warning>
  **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.
</Warning>

### 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.

<Note>
  `xrpl-py` is still on 5.1.0 and did not receive either fix. Keep Python out of the signing path.
</Note>

## Steps

<Steps>
  <Step title="Verify the toolchain">
    Run this first, every session, before trusting anything else. It drives the entire lending chain against live Devnet with no workarounds: `VaultCreate` → `VaultDeposit` → `LoanBrokerSet` → `LoanBrokerCoverDeposit` → `LoanSet` with a native counterparty signature.

    <CodeGroup>
      ```bash npm theme={null}
      npm run verify
      ```
    </CodeGroup>

    A successful run looks like this (elapsed times vary with Devnet close times):

    ```text theme={null}
    [39.2s] vault+deposit+broker+cover all tesSUCCESS, zero workarounds
    For LoanSet transaction the auto calculated Fee accounts for total number of signers...
    [47.1s] LoanSet tesSUCCESS  loan=86C4316664B46577…

    CONFIRMED: xrpl@5.2.0 signs the counterparty signature natively. Both recon workarounds are OBSOLETE.
    ```

    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.
  </Step>

  <Step title="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.

    <CodeGroup>
      ```bash Terminal theme={null}
      node src/index.mjs
      ```
    </CodeGroup>

    You should see:

    ```text theme={null}
    serving 4 facility(ies) baked into demo/
    [22:20:18] INFO  [api] api listening {"port":8787,"source":"devnet"}
    [22:20:22] INFO  [main] up {"vaults":4,"api":"http://localhost:8787","publishing":false}
    ```

    `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:

    | File                       | Label                        | Vault ID             | What it demonstrates                                                |
    | -------------------------- | ---------------------------- | -------------------- | ------------------------------------------------------------------- |
    | `demo/indexer-race.json`   | Meridian Trade Finance I     | `864C5A2D…E37E8C835` | First impairment with `PreviousFields: {}`                          |
    | `demo/ordering-vault.json` | Kestrel Bridge Financing II  | `24EAA01A…1E5810089` | Two defaults declared largest-first, conduct grade E                |
    | `demo/metadata-vault.json` | Calder Structured Credit III | `5763707D…2A96CB0E5` | Share token whose own metadata carries a valuation pointer          |
    | `demo/gate-vault.json`     | Thorne Senior Secured I      | `4A5A8E37…0190DF5`   | XLS-70 credential plus permissioned domain, entry refused on-ledger |

    <Warning>
      `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.
    </Warning>

    You can override the auto-discovered facilities with explicit arguments or an environment variable:

    <CodeGroup>
      ```bash Argument theme={null}
      node src/index.mjs --vault 864C5A2DDCED78C6198B0703169D533B747A56E5F9398D832033F79E37E8C835:"Meridian Trade Finance I"
      ```

      ```bash Environment variable theme={null}
      VAULTS=<64-hex-id>,<64-hex-id> node src/index.mjs
      ```
    </CodeGroup>
  </Step>

  <Step title="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:**

    ```bash theme={null}
    curl -s http://localhost:8787/api/health
    ```

    ```json theme={null}
    {
      "ok": true,
      "contractVersion": "1.0.0",
      "source": "devnet",
      "ledgerIndex": 5263343,
      "serverTime": "2026-09-12T22:20:22Z",
      "buildVersion": "3.4.0-rc5"
    }
    ```

    `ok` stays `false` until the first poll completes — this is intentional, not a bug.

    **Read all four grades, sorted worst first:**

    ```bash theme={null}
    curl -s http://localhost:8787/api/vaults \
      | jq -r '.vaults[] | "\(.grade)\t\(.gradeNumeric)\t\(.navNaive)\t\(.navCorrect)\t\(.navDivergenceBps)bp\t\(.label)"'
    ```

    ```text theme={null}
    B   26  0.210000  0.210000     0bp  Kestrel Bridge Financing II
    AA  89  1.000000  0.803922  1961bp  Meridian Trade Finance I
    AA  89  1.000000  0.803922  1961bp  Calder Structured Credit III
    AAA 100 1.000000  1.000000     0bp  Thorne Senior Secured I
    ```

    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, $\rho = 79\%$) is what catches it.

    **Value a pledge from the share token ID alone:**

    ```bash theme={null}
    curl -s "http://localhost:8787/api/mpt/000000014D667775372D5B78E07FFF294678C7F9CE82AFBC/nav?units=1000000" \
      | jq '{unitValue, pledge}'
    ```

    ```json theme={null}
    {
      "unitValue": {
        "held": "0.803922",
        "reported": "1.000000",
        "divergenceBps": 1961,
        "basis": "assets net of recognised loss, divided by units outstanding"
      },
      "pledge": {
        "units": "1000000",
        "valueHeld": "803922",
        "valueReported": "1000000",
        "overstatement": "196078"
      }
    }
    ```

    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:**

    ```bash theme={null}
    curl -s http://localhost:8787/api/vaults/4A5A8E3716D52E334AEB077ADB09456EF4A941CC26021A8EAD37F95B00190DF5/gate
    ```

    ```json theme={null}
    {
      "gated": true,
      "private": true,
      "domainId": "FFBEC89D98B4E7CF52F4F254235086514A90CED0EC531941BDF747AF40C5A2FB",
      "acceptedCredentials": [
        { "issuer": "rKQjjU5KFs9RAZCDvYVjcaoVK5gGsCJgkP", "type": "ORMA-IG", "typeHex": "4F524D412D4947" }
      ],
      "domainOwner": "rGFFSqqY1R3764FKF7crU5F6bYh9qaD45S",
      "issuerNamed": true
    }
    ```

    `domainOwner` is the vault owner, not Orma. They named our issuer unilaterally — we signed nothing and cannot refuse to be cited. See [Gate](/gate).
  </Step>

  <Step title="Start the frontend (optional)">
    The frontend is a separate package. Run it from the `app/` subdirectory.

    <CodeGroup>
      ```bash Terminal theme={null}
      cd app
      pnpm install
      pnpm dev
      ```
    </CodeGroup>

    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`.

    <Note>
      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.
    </Note>
  </Step>

  <Step title="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:

    ```bash theme={null}
    PUBLISH_SEED=sEd7...
    ```

    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.

    <Warning>
      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.
    </Warning>

    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.
  </Step>
</Steps>

## Environment variables

All variables are optional.

| Variable         | Default                               | Effect                                                                                     |
| ---------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ |
| `PORT`           | `8787`                                | API listening port                                                                         |
| `XRPL_WS`        | `wss://s.devnet.rippletest.net:51233` | Upstream `rippled` WebSocket                                                               |
| `POLL_MS`        | `4000`                                | Vault re-read interval in ms. The reader re-reads full state, never diffs `PreviousFields` |
| `VAULTS`         | (auto from `demo/`)                   | Comma-separated 64-hex vault IDs, overriding facility discovery                            |
| `PUBLISH_SEED`   | (unset)                               | XRPL family seed for the oracle publisher account                                          |
| `LOG_PRETTY`     | (unset)                               | Set to `1` for coloured human-readable logs instead of JSON lines                          |
| `LOG_LEVEL`      | `info`                                | `debug`, `info`, `warn`, or `error`                                                        |
| `WATCH_ACCOUNTS` | (empty)                               | Comma-separated accounts to scan for escrowed share collateral                             |
| `RATER_ADDRESS`  | from `demo/gate-vault.json`           | Whose credential the gate route checks for                                                 |

## 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.
