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

# Gate Vault Deposits with Orma's ORMA-IG Credentials

> How Orma's XLS-70 credential (ORMA-IG) and an XLS-80 permissioned domain enforce credit standards at the ledger level — no middleware, no approval flow.

A vault owner who wants graded capital can enforce Orma's credit standards at the ledger level, without involving Orma in any individual deposit decision. A limited partner who holds an accepted `ORMA-IG` credential submits a `VaultDeposit` and the transaction succeeds. A limited partner without one submits the exact same transaction for the same amount, and the ledger returns `tecNO_AUTH`. Orma is not consulted when that happens. No API of ours is in the path. The difference between a grade and a gate is that a gate does not require anyone to read the grade.

<Note>
  XLS-65 vaults and XLS-66 lending are not on Mainnet, so every vault referenced on this page is a Devnet vault (network ID 2, `wss://s.devnet.rippletest.net:51233`, rippled 3.4.0-rc5). The two standards the gate is built from — XLS-70 credentials and XLS-80 permissioned domains — are live on Mainnet today. The vault underneath is what keeps this page Devnet-only.
</Note>

## The four-transaction chain

Four transactions, signed by three different parties, none of whom has to trust the others:

| # | Transaction                                        | Signed by        | What it does                                                                                  |
| - | -------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------- |
| 1 | `CredentialCreate`                                 | The rater (Orma) | Issues the credential to a named LP; nothing is enforced yet                                  |
| 2 | `CredentialAccept`                                 | The LP           | Accepts the credential; without this step the gate remains closed to them even after step 1   |
| 3 | `PermissionedDomainSet`                            | The vault owner  | Creates a domain citing Orma's issuer address; Orma signs nothing and cannot decline          |
| 4 | `VaultCreate` with `tfVaultPrivate` and `DomainID` | The vault owner  | Creates the vault attached to the domain; the private flag is required or the domain is inert |

<Warning>
  `DomainID` without `tfVaultPrivate` does not gate anything. The vault is created, the domain is attached, every read shows the domain ID, and every depositor is admitted. There is no error and no warning. Orma's gate reader treats this as its own named outcome and reports it in the `note` field: `carries a domain but is not flagged private, so nothing is enforced`.
</Warning>

## The `ORMA-IG` credential type

Credential types in XLS-70 are hex-encoded UTF-8 blobs of 1–64 bytes. Orma uses:

| Credential type | Hex encoding         | What it certifies                                          |
| --------------- | -------------------- | ---------------------------------------------------------- |
| `ORMA-IG`       | `4F524D412D4947`     | The LP may deposit into any facility graded BBB- or better |
| `ORMA-SPEC`     | `4F524D412D53504543` | The LP may deposit into speculative-grade facilities       |

The credential is keyed to a **bar**, not to a facility. One credential is reusable across every domain that cites Orma's issuer address. A per-facility credential would be a whitelist wearing a credential's clothes.

## Step 1: Issue the credential

```js theme={null}
export const hexType = (s) => Buffer.from(s, 'utf8').toString('hex').toUpperCase()

export const issueCredential = (client, issuer, subject, type) =>
  submitValidated(client, issuer, {
    TransactionType: 'CredentialCreate',
    Subject: subject,
    CredentialType: hexType(type),  // 'ORMA-IG' → '4F524D412D4947'
  })
```

Issuing the credential creates it on the ledger. The LP's deposits are still `tecNO_AUTH` until step 2.

## Step 2: The LP accepts

```js theme={null}
export const acceptCredential = (client, subject, issuerAddress, type) =>
  submitValidated(client, subject, {
    TransactionType: 'CredentialAccept',
    Issuer: issuerAddress,
    CredentialType: hexType(type),
  })
```

A credential is not something that can be done *to* someone. The rater asserts, the subject accepts, and only both together admit anyone anywhere. This is the step that is easy to skip when reading the spec, and it is the one that decides whether the gate works.

## Step 3: The vault owner creates a domain

```js theme={null}
export async function createDomain(client, owner, accepted) {
  const r = await submitValidated(client, owner, {
    TransactionType: 'PermissionedDomainSet',
    AcceptedCredentials: accepted.map((a) => ({
      Credential: { Issuer: a.issuer, CredentialType: hexType(a.type) },
    })),
  })
  if (!r.ok) return r
  const node = (r.meta.AffectedNodes ?? [])
    .map((n) => n.CreatedNode)
    .find((n) => n?.LedgerEntryType === 'PermissionedDomain')
  return { ...r, domainId: node?.LedgerIndex ?? null }
}
```

The domain ID is not returned as a transaction field. Retrieve it from the `LedgerIndex` of the `PermissionedDomain` node in `AffectedNodes`.

Orma signs nothing in this step. The vault owner cites Orma's issuer address; Orma is not consulted and cannot decline. This is the correct shape for a rating: a prospectus can reference a rating the agency was never asked about. It also means Orma cannot sell placement in a domain — we do not control who lists us.

## Step 4: Create the vault with the private flag

```js theme={null}
export const TF_VAULT_PRIVATE = 0x00010000  // 65536

await submitValidated(client, vaultOwner, {
  TransactionType: 'VaultCreate',
  Asset: { currency: 'XRP' },
  VaultKind: 1,
  SubscriptionDate: SUB,
  RedemptionDate: RED,
  WithdrawalPolicy: 1,
  Flags: TF_VAULT_PRIVATE,   // REQUIRED alongside DomainID
  DomainID: dom.domainId,
})
```

## The measured outcome

Two limited partners, funded from the same faucet, depositing 20 XRP each into the same vault, submitted concurrently:

| Depositor                            | Holds `ORMA-IG`? | Amount | Validated result |
| ------------------------------------ | ---------------- | ------ | ---------------- |
| `rnc7dQyrcFyXE9nyYbRaJ8tV3CZcFNEsfq` | Yes, accepted    | 20 XRP | `tesSUCCESS`     |
| `ramrvatE3yCyW7S7Ry3rCTC5eXaaHEG1tn` | No               | 20 XRP | `tecNO_AUTH`     |

The second transaction was refused by the ledger, not by Orma.

## Revocation is asymmetric by design

`CredentialDelete` removes the credential. The LP's next deposit bounces. Their existing position is untouched and withdraws normally.

| Step                  | Transaction                       | Result       |
| --------------------- | --------------------------------- | ------------ |
| Revoke                | `CredentialDelete` (issuer signs) | `tesSUCCESS` |
| LP tries to add 3 XRP | `VaultDeposit`                    | `tecNO_AUTH` |
| LP takes 5 XRP out    | `VaultWithdraw`                   | `tesSUCCESS` |

```js theme={null}
export const revokeCredential = (client, issuer, subject, type) =>
  submitValidated(client, issuer, {
    TransactionType: 'CredentialDelete',
    Subject: subject,
    CredentialType: hexType(type),
  })
```

The gate controls who may **enter**. It never controls who may leave.

This is the only acceptable design. Orma exists because the `LoanBroker` owner — who is necessarily the vault owner — holds discretion over other people's money: they choose when a loss is recognised and in what order losses are realised, and both choices move value from investors to them. A rater who could revoke a credential and thereby trap an LP's capital inside a vault would be a second party with exactly that shape of power, created by the tool built to expose the first. That would be a worse problem than the one being solved.

Symmetric revocation also inverts the incentive at the moment it matters most. A rater who downgrades a deteriorating facility should make it harder for **new** money to walk in, not harder for **existing** money to walk out. Entry-only revocation means the worst thing a hostile or mistaken rater can do is deny someone an allocation — a cost an LP can price. Being unable to redeem is not.

<Note>
  The revocation asymmetry is a property of XLS-70 and XLS-65 composing, not something Orma implements. There is no Orma code path that could make withdrawal conditional on a credential even if we wanted one. Orma verified it holds rather than assuming it, because the whole claim depends on it.
</Note>

## The three traps

### Trap 1: DomainID is not on the Vault object

The obvious lookup — read the `Vault` ledger entry and check for `DomainID` — returns `undefined`. The field lives on the **share `MPTokenIssuance`**, not on the Vault. An absent `DomainID` on the Vault reads identically to "open to everyone", even when the vault is gated.

Orma's `gateStatus()` function does the correct lookup:

```js theme={null}
const issuance = await xrpl.mptIssuance(vault.shareMptId)   // ledger_entry, mpt_issuance
const domainId = issuance.DomainID ?? null
if (!domainId) { /* genuinely ungated */ }
const dom = await xrpl.req({ command: 'ledger_entry', index: domainId })
out.domainOwner = dom.result.node.Owner ?? null
out.acceptedCredentials = (dom.result.node.AcceptedCredentials ?? []).map((a) => ({
  issuer: a.Credential?.Issuer ?? null,
  type: a.Credential?.CredentialType ? readType(a.Credential.CredentialType) : null,
  typeHex: a.Credential?.CredentialType ?? null,
}))
out.gated = out.private && out.acceptedCredentials.length > 0
```

<Warning>
  This failure mode points the wrong way. A wrong answer that says "gated" when a vault is open is visibly wrong the moment someone deposits successfully. A wrong answer that says "open" when a vault is gated looks correct until a deposit is refused — and then looks like the ledger is broken. Always resolve `gated` from two independent reads: the private flag on the Vault and the accepted credential list on the domain.
</Warning>

### Trap 2: `engine_result` is not the authoritative result

`submit` returns a provisional `engine_result`. Orma observed a provisional `engine_result` of `tecNO_AUTH` go on to validate as `tesSUCCESS`. On any other feature that is a nuisance; on this one it is the difference between "the ledger refused them" and "the ledger let them in", reported with full confidence, in the wrong direction.

Always read the result from `meta.TransactionResult` on the **validated** transaction:

```js theme={null}
while (Date.now() - t0 < timeoutMs) {
  await new Promise((r) => setTimeout(r, 1000))
  const r = await client.request({ command: 'tx', transaction: hash })
  if (r.result.validated) {
    const code = r.result.meta.TransactionResult   // the authoritative answer
    return { ok: code === 'tesSUCCESS', code, hash, meta: r.result.meta, engine }
  }
}
```

### Trap 3: Phase rules mask the credential rule

A closed-ended vault has three phases. Two phase rules return different error codes that can be mistaken for evidence the gate is not working:

* `VaultDeposit` outside the Subscription window returns `tecEXPIRED`. The credential is never consulted.
* `VaultWithdraw` during the Investment phase returns `tecTOO_SOON`, regardless of credentials.

Run the entire deposit and withdrawal sequence inside the Subscription window so `tecNO_AUTH` and `tesSUCCESS` mean what they appear to mean.

<Note>
  `tecNO_AUTH` tells you something about credentials. `tecEXPIRED` and `tecTOO_SOON` tell you something about the calendar. Know which you are reading before drawing conclusions about the gate.
</Note>

## Checking if a vault is gated

Use `GET /api/vaults/:vaultId/gate`. This route performs the two extra ledger reads — the share issuance and the domain — that `GET /api/vaults/:vaultId` does not:

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

```json theme={null}
{
  "serverTime": "2026-09-12T22:20:22Z",
  "ledgerIndex": 5263343,
  "vaultId": "4A5A8E3716D52E334AEB077ADB09456EF4A941CC26021A8EAD37F95B00190DF5",
  "gated": true,
  "private": true,
  "domainId": "FFBEC89D98B4E7CF52F4F254235086514A90CED0EC531941BDF747AF40C5A2FB",
  "acceptedCredentials": [
    {
      "issuer": "rKQjjU5KFs9RAZCDvYVjcaoVK5gGsCJgkP",
      "type": "ORMA-IG",
      "typeHex": "4F524D412D4947"
    }
  ],
  "domainOwner": "rGFFSqqY1R3764FKF7crU5F6bYh9qaD45S",
  "note": null,
  "issuerNamed": true,
  "raterAddress": "rKQjjU5KFs9RAZCDvYVjcaoVK5gGsCJgkP"
}
```

`issuerNamed` answers Orma's own question — am I cited in this domain — without asking the vault owner. It is `true` here, and the `domainOwner` (`rGFFSqqY1R3764FKF7crU5F6bYh9qaD45S`) never signed anything of ours to make it so.

The `note` field is `null` when the gate is working correctly. It is non-null when the answer needs a caveat:

| `note`                                                                | What happened                                 |
| --------------------------------------------------------------------- | --------------------------------------------- |
| `no share issuance to read a domain from`                             | The vault has no share `MPTokenIssuance`      |
| `open to any depositor`                                               | No `DomainID` on the issuance and not private |
| `marked private but carries no domain, so nothing is enforced`        | `tfVaultPrivate` set, no domain attached      |
| `carries a domain but is not flagged private, so nothing is enforced` | Trap 1 from above — the domain is inert       |

This route caches for 30 seconds. A domain changes when someone explicitly edits it, not on every ledger close.

## What this gate does not do

<CardGroup cols={2}>
  <Card title="Does not run on Mainnet" icon="circle-xmark">
    XLS-65 and XLS-66 are not on Mainnet, so the vault this gate protects cannot exist there yet. XLS-70 credentials and XLS-80 permissioned domains are already live on Mainnet — the enforcement half has a production path ahead of the measurement half.
  </Card>

  <Card title="Does not make the grade authoritative" icon="circle-xmark">
    A vault owner chooses whether to name Orma's issuer address, and can remove us from the domain with a single `PermissionedDomainSet` we never see coming. The grade binds only where someone has decided it should.
  </Card>

  <Card title="Does not restrict withdrawal" icon="circle-xmark">
    By design, and no configuration changes this. There is no Orma code path that could make withdrawal conditional on a credential.
  </Card>

  <Card title="Does not certify the facility" icon="circle-xmark">
    The `ORMA-IG` credential is issued to an LP and asserts what that LP may enter. The facility measurement is published separately as an XLS-47 Oracle object.
  </Card>
</CardGroup>
