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

# NAV Pointer: Self-Describing Vault Share Collateral

> How Orma embeds a valuation URL template in a vault share's immutable MPTokenMetadata so any holder can price the collateral without knowing Orma exists.

When someone pledges vault shares to a second lender, that lender has an `MPTokenIssuanceID` and nothing else. The share token carries no price field. The `MPTokenIssuance` records who issued it and how many units are outstanding, but says nothing about what a unit is worth. Orma solves this by writing a **pointer** into the share token's own `MPTokenMetadata` at vault creation — not a value, a URL template. Whoever holds the token reads its metadata off the ledger, substitutes the issuance ID they already have into the template, follows it, and receives a valuation computed from public ledger state. Nothing in that chain requires the lender to know Orma exists.

## Two constraints that shape the whole design

### The metadata is write-once, permanently

`MPTokenMetadata` can only be set on `VaultCreate`. Three transactions on Devnet confirm there is no correction path:

| Attempt                                 | Result                                                                              |
| --------------------------------------- | ----------------------------------------------------------------------------------- |
| `MPTokenMetadata` set on `VaultCreate`  | `tesSUCCESS` — persists on the share `MPTokenIssuance`                              |
| `VaultSet` updating it later            | Rejected at deserialisation: `Field 'MPTokenMetadata' found in disallowed location` |
| `MPTokenIssuanceSet` by the vault owner | `tecNO_PERMISSION`                                                                  |

The third result is the important one. The issuer of a vault share is the vault's **pseudo-account**, which carries `lsfDisableMaster` and has no regular key. No key in existence can sign an update. The field is frozen for the life of the vault.

<Warning>
  A number written into share metadata is a lie within one ledger close. There is no correction path, no admin override, and no migration. Never put a NAV value, an audit date, or a grade in `MPTokenMetadata`.
</Warning>

### The pointer cannot name the thing it describes

`MPTokenMetadata` must be composed at the moment of `VaultCreate` submission — **before** the vault exists. At that moment the `VaultID` and the share `MPTokenIssuanceID` are both unknown (assigned by the transactor), and the field can never be corrected once either becomes known.

A self-referential URL is therefore impossible. Instead, Orma writes a template with a placeholder:

```
http://localhost:8787/api/mpt/{mpt_issuance_id}/nav
```

The reader supplies the missing half. Anyone reading this metadata already holds the issuance ID (that's how they found the metadata), so the pointer only has to name the service and the substitution rule.

## The pointer structure

The full metadata written to a vault share — XLS-89 conformance fields first, then one namespaced `orma` block:

```json theme={null}
{
  "ticker": "ORMA-CSC3",
  "name": "Calder Structured Credit III",
  "icon": "http://localhost:8787/assets/mark.svg",
  "asset_class": "rwa",
  "issuer_name": "Orma",
  "desc": "Redeemable claim on a closed-ended private credit facility. Valuation is net of recognised loss.",
  "orma": {
    "v": 1,
    "instrument": "vault-share",
    "nav_url": "http://localhost:8787/api/mpt/{mpt_issuance_id}/nav",
    "nav_url_param": "{mpt_issuance_id}",
    "nav_basis": "assets net of recognised loss, divided by units outstanding",
    "doc": "https://orma.credit/nav"
  }
}
```

<Accordion title="Why the XLS-89 fields?">
  rippled volunteers a warning when metadata is not XLS-89 shaped: it reports the token "might not be discoverable by Explorers and Indexers" and lists the five fields it expects — `ticker`, `name`, `icon`, `asset_class`, `issuer_name`. A vault share created without metadata is invisible to every indexer that reads the standard. Five fields is a cheap price for being listed.

  `asset_class: "rwa"` is the closest true statement for a private credit facility (a real-world asset). The exact instrument type lives in `orma.instrument` as `vault-share`. The `xls89Report()` API response carries a `taxonomyNote` field explaining this so consumers are not misled by the coarser field.
</Accordion>

The `nav_url_param` field exists so a consumer who has never read this page can still identify which substring to replace: substitute `nav_url_param`'s value with the issuance ID you hold.

## How a second lender resolves the pointer

A lender holds an MPT issuance ID and nothing else — no relationship with the facility, no access to its reporting, no reason to trust any figure someone hands them.

```js theme={null}
import { Client } from 'xrpl'

const ISSUANCE = '000000014D667775372D5B78E07FFF294678C7F9CE82AFBC'

const c = new Client('wss://s.devnet.rippletest.net:51233')
await c.connect()

// 1. Read the MPTokenIssuance off the ledger
const { result } = await c.request({ command: 'ledger_entry', mpt_issuance: ISSUANCE })
const node = result.node

// 2. Decode MPTokenMetadata (hex-encoded UTF-8)
const meta = JSON.parse(Buffer.from(node.MPTokenMetadata, 'hex').toString('utf8'))

// 3. Substitute the issuance ID you already hold into the template
const url = meta.orma.nav_url.replaceAll(meta.orma.nav_url_param, ISSUANCE)

// 4. Follow it
const nav = await (await fetch(url, { headers: { accept: 'application/json' } })).json()

console.log(nav.unitValue.held, nav.unitValue.reported, nav.unitValue.divergenceBps)
// 0.803922  1.000000  1961

await c.disconnect()
```

Every step before the last is public ledger state. The last is a URL the ledger named. Nothing in this loop requires Orma to be known to the lender in advance — that is the difference between a dashboard (which you have to already know about) and a primitive (reachable from the asset itself).

### Using the API

To run the resolution loop via Orma's API, call `GET /api/mpt/:mptId/resolve`:

```bash theme={null}
curl -s "http://localhost:8787/api/mpt/000000014D667775372D5B78E07FFF294678C7F9CE82AFBC/resolve"
```

```json theme={null}
{
  "serverTime": "2026-09-12T22:20:22Z",
  "ledgerIndex": 5263343,
  "issuanceId": "000000014D667775372D5B78E07FFF294678C7F9CE82AFBC",
  "issuer": "r3hE8HanpccSZdgmeHCfYFEkwxFjdDmmvt",
  "unitsOutstanding": "51000000",
  "resolved": true,
  "navUrl": "http://localhost:8787/api/mpt/000000014D667775372D5B78E07FFF294678C7F9CE82AFBC/nav",
  "steps": [
    { "step": "read MPTokenIssuance", "ok": true,
      "detail": "issuer r3hE8HanpccSZdgmeHCfYFEkwxFjdDmmvt, 51000000 units outstanding" },
    { "step": "decode metadata", "ok": true, "detail": "valid JSON" },
    { "step": "XLS-89 conformance", "ok": true, "detail": "asset_class rwa" },
    { "step": "find valuation pointer", "ok": true,
      "detail": "http://localhost:8787/api/mpt/000000014D667775372D5B78E07FFF294678C7F9CE82AFBC/nav" },
    { "step": "follow pointer", "ok": true, "detail": "unit value 0.803922" }
  ]
}
```

The `issuer` on the first step — `r3hE8HanpccSZdgmeHCfYFEkwxFjdDmmvt` — is the vault pseudo-account, the same account whose `lsfDisableMaster` flag makes the metadata permanent.

<Tip>
  The `/resolve` and `/nav` routes are kept separate deliberately. `/nav` is what the template resolves to and never follows a pointer itself (a valuation reachable only by walking a pointer to itself is a loop). `/resolve` is the diagnostic that walks every step. Both send open CORS headers — the entire point is that a party with no relationship to Orma can call them.
</Tip>

## Valuing a pledge

To value a specific number of units — for example, 1,000,000 units pledged as collateral — call `GET /api/mpt/:mptId/nav?units=1000000`:

| Line                              | Drops     | XRP      |
| --------------------------------- | --------- | -------- |
| Value on the reported figure      | 1,000,000 | 1.000000 |
| Value on the honest (held) figure | 803,922   | 0.803922 |
| Overstatement avoided             | 196,078   | 0.196078 |

19.61% of the stated collateral value was not there. A haircut does not fix this: a haircut applies to the held value and absorbs volatility in a number that is correct. It does not absorb a number that is wrong.

The valuation response carries a `provenance` block with the three raw ledger integers and a `recompute` formula, so you can verify the figure yourself without trusting Orma:

```json theme={null}
{
  "unitValue": {
    "held": "0.803922",
    "reported": "1.000000",
    "divergenceBps": 1961,
    "basis": "assets net of recognised loss, divided by units outstanding"
  },
  "provenance": {
    "network": "devnet",
    "assetsTotal": "51000000",
    "lossUnrealized": "10000000",
    "unitsOutstanding": "51000000",
    "recompute": "(assetsTotal - lossUnrealized) / unitsOutstanding",
    "buildVersion": "3.4.0-rc5"
  }
}
```

Read `AssetsTotal` and `LossUnrealized` from the `Vault` object on any Devnet node and apply the one-line formula in `recompute`. The numbers should match to the drop.

## When resolution fails

Every failure mode is a normal outcome, not an error. The resolver reports a failed step and continues so the lender receives the most useful answer possible.

<Accordion title="Failure mode reference">
  | What happened                                          | What the resolver reports                                                     |
  | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
  | The ID is not an `MPTokenIssuance`                     | Step 1 fails with the rippled error; `resolved: false`                        |
  | `MPTokenMetadata` is absent                            | `no metadata on the issuance`                                                 |
  | The blob is not valid hex                              | `metadata is not valid hex`, first 64 characters echoed back                  |
  | The blob is plain text, not JSON                       | `metadata is not JSON, so it is not XLS-89 shaped`                            |
  | JSON, but no `orma.nav_url`                            | `no orma.nav_url in the metadata`; `resolved: false`; metadata still returned |
  | The pointer URL is a template and no ID was supplied   | The pointer is not followed                                                   |
  | The pointer uses a scheme other than `http` or `https` | Ignored — this value was written by a third party and reaches a `fetch`       |
  | The endpoint does not answer within 2,500 ms           | `timeout`                                                                     |
  | The endpoint returns a non-2xx status                  | `HTTP <status>`                                                               |
  | The endpoint returns 200 with no `unitValue`           | `answered, but the document carries no unitValue`                             |
</Accordion>

If the endpoint answers but carries no `unitValue`, the pledge value response returns an explicit `unpriced` string rather than a number or an exception — a lender's tool has to be able to say "I could not value this".

## Embedding the pointer at vault creation

Build and encode the metadata before submitting `VaultCreate`:

```js theme={null}
// MPT_PLACEHOLDER is the literal string that stands in for the issuance ID.
// It must match the nav_url_param field written into the metadata so consumers
// know which substring to replace when they resolve the pointer later.
const MPT_PLACEHOLDER = '{mpt_issuance_id}'

const meta = {
  ticker: 'ORMA-CSC3',
  name: 'Calder Structured Credit III',
  icon: `${BASE_URL}/assets/mark.svg`,
  asset_class: 'rwa',
  issuer_name: 'Orma',
  desc: 'Redeemable claim on a closed-ended private credit facility. Valuation is net of recognised loss.',
  orma: {
    v: 1,
    instrument: 'vault-share',
    nav_url: `${BASE_URL}/api/mpt/${MPT_PLACEHOLDER}/nav`,
    nav_url_param: MPT_PLACEHOLDER,
    nav_basis: 'assets net of recognised loss, divided by units outstanding',
    doc: 'https://orma.credit/nav',
  },
}

// Encode as hex-encoded UTF-8 (the ledger ceiling is 1,024 bytes — verify before submitting)
const blob = Buffer.from(JSON.stringify(meta), 'utf8').toString('hex').toUpperCase()

await client.submitAndWait({
  TransactionType: 'VaultCreate',
  Account: owner.address,
  Asset: { currency: 'XRP' },
  VaultKind: 1,
  SubscriptionDate: SUB,
  RedemptionDate: RED,
  WithdrawalPolicy: 1,
  MPTokenMetadata: blob,
}, { wallet: owner, autofill: true })
```

Verify the encoded blob does not exceed the ledger's 1,024-byte ceiling before submitting. Just under the ceiling is a permanent record you cannot trim later — validate the length and abort rather than submit an oversized blob.

<Warning>
  `BASE_URL` must be a hostname you are willing to keep serving for the full life of the vault. The pointer is permanent. Writing a host you might move off is the one mistake this design cannot recover from.
</Warning>

`VaultKind: 1` with `SubscriptionDate` and `RedemptionDate` is required. A vault that is not closed-ended cannot have a `LoanBroker`, so every vault whose shares are worth valuing this way is a fixed-term fund.

## API routes for second lenders

<CardGroup cols={2}>
  <Card title="GET /api/mpt/:mptId/nav" icon="chart-line">
    Returns the current unit value (held and reported), divergence in basis points, and the raw provenance inputs. Add `?units=N` to value a specific pledge.
  </Card>

  <Card title="GET /api/mpt/:mptId/resolve" icon="magnifying-glass">
    Traces every step of the resolution loop: reads the issuance, decodes the metadata, checks XLS-89 conformance, finds the pointer, and follows it. Add `?resolve=false` to stop at the pointer without following it.
  </Card>
</CardGroup>

Both routes are read-only views of public ledger state and send open CORS headers.
