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

# Inspect agent exposure without signing

> Inspect delegated permissions, spendable XRP and contingent refills at one ledger snapshot, with exact values and explicit incomplete coverage.

Reins now has a read-only exposure inspector. You can inspect delegated permissions, allowance funds and contingent refill caps without loading a wallet or signing a transaction. It reads account, object and fee data at one validated ledger hash and returns exact XRP amounts as integer-drop strings.

> [!NOTE]
>
> Published in Reins 0.0.3 on November 9, 2026. All four npm package versions and tarball integrities were verified against the checked release artifacts. All 146 offline tests, the build, a fresh registry installation, runtime checks and strict TypeScript checks passed. Testnet alpha. No external security review.

## Use the published CLI

With Node.js 22.12 or later and your existing testnet policy:

```bash theme={"dark"}
npx reinskit@0.0.3 inspect reins.yaml
npx reinskit@0.0.3 inspect reins.yaml --json
```

No wallet file is needed. The command reads public ledger state and returns a nonzero exit status for incomplete coverage or delegated-permission drift.

## Run the copyable implementation

You need Node.js 22.12 or later and an existing testnet `reins.yaml` containing your account addresses. This guide does not create or fund accounts. Install the already-published dependency and runner in a new local project:

```bash theme={"dark"}
npm install @reinskit/policy@0.0.3 xrpl@^5.3.0 tsx
```

Copy the implementation at the end of this page into `inspect.ts`. Save this runner as `run-inspect.mts`:

```typescript theme={"dark"}
import { readFileSync } from 'node:fs'
import { Client } from 'xrpl'
import { parsePolicy } from '@reinskit/policy'
import { inspectPolicy } from './inspect.js'

const policy = parsePolicy(readFileSync('reins.yaml', 'utf8'))
if (policy.network !== 'testnet') throw new Error('This example is testnet only')
const client = new Client('wss://s.altnet.rippletest.net:51233', { timeout: 30000 })
await client.connect()
try {
  const report = await inspectPolicy(client, policy)
  console.log(JSON.stringify(report, null, 2))
  if (!report.complete || report.policyMatches === false) process.exitCode = 1
} finally {
  await client.disconnect()
}
```

```bash theme={"dark"}
node --import tsx run-inspect.mts
```

The runner makes public read requests only. It has no seed, wallet loading, autofill or submission call. Offline tests and an offline synthetic example have passed; this new inspector has not yet been checked against a live RPC endpoint.

## CLI from an updated source checkout

If you already have the updated Reins source checkout, build and run:

```bash theme={"dark"}
npm run build
node packages/cli/dist/cli.js inspect reins.yaml
node packages/cli/dist/cli.js inspect reins.yaml --json
```

The command accepts `--endpoint` for an endpoint override. It does not accept a wallet file. Exit status is nonzero when coverage is incomplete or delegated permissions differ from policy intent. The `policyMatches` field covers those permissions, not comprehensive policy verification. Use [verify](https://docs.reinskit.com/reference/cli.md#verify) for the existing policy comparison workflow.

## Read the result

The report has `schemaVersion: 1`, snapshot provenance, account roles, live permissions, `exposure`, findings and limitations. Its exposure fields are decimal strings in drops, or `null` when unknown:

| Field | Meaning |
| - | - |
| `spendableDrops` | Allowance XRP balance minus snapshot account and object reserve. |
| `refillDrops` | Contingent XRP refill cap reachable through supported CheckCash permissions. |
| `reachableDrops` | Scoped allowance exposure through the observed supported permissions. |
| `feeFloatDrops` | Full XRP balance on the agent account, shown separately. |

A synthetic offline fixture with 3 XRP spendable, a 5 XRP refill cap and 1.5 XRP on the agent reports 8 XRP of scoped reachable allowance exposure. These are fixture values, not live ledger evidence. Agent funds remain separate.

## Understand incomplete coverage

`complete: true` means the supported observation scope was covered. It does not mean safe, policy-compliant or independently verified. `null` is unknown, not zero.

This first inspector is XRP-only. Unsupported issued assets, escrow permissions, signing topologies, sponsorship, extra delegates and unsupported ledger objects return incomplete coverage. The starter policy includes `EscrowFinish`, so it can legitimately produce an incomplete report. Do not remove an operational permission merely to obtain a complete report.

Expired refill Checks are excluded using snapshot ledger time. Refill caps are contingent, not reserved funds. Their conservative estimate is capped by the current vault balance; actual cashable funds can be lower. Future funding and later ledger changes are excluded. Network identity is declared by policy rather than independently established from the endpoint.

Ledger permissions and balances are observed state. The exposure calculation is Reins toolkit interpretation. Independent per-payment, payee and rolling-velocity co-signer rules remain planned. Read [enforcement](https://docs.reinskit.com/concepts/enforcement.md) before relying on the account model.

## Copyable implementation

This is the reviewed XRP-only inspector source, with its policy type import adjusted to the published package. It performs read requests only. Save the complete block as `inspect.ts`.

> [accordion] Read-only inspector source (TypeScript)
>
> ```typescript theme={"dark"}
> /** Read-only XRP topology observations at one validated ledger. Not a universal loss bound. */
> import type { Client } from 'xrpl';
> import type { ReinsPolicy } from '@reinskit/policy';
> export interface InspectReport {
>     schemaVersion: 1;
>     complete: boolean;
>     policyMatches?: boolean;
>     snapshot: {
>         ledgerIndex: number;
>         ledgerHash: string;
>         closeTime: number;
>         network: string;
>     };
>     accounts: {
>         vault: string;
>         allowance: string;
>         agent: string;
>     };
>     permissions: string[];
>     exposure: {
>         spendableDrops: string | null;
>         refillDrops: string | null;
>         reachableDrops: string | null;
>         feeFloatDrops: string | null;
>     };
>     findings: {
>         code: string;
>         message: string;
>     }[];
>     limitations: string[];
> }
> type Json = Record<string, unknown>;
> const FEES = '4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A651';
> const SUPPORTED = new Set(['Payment', 'CheckCash', 'EscrowFinish']);
> function object(v: unknown): Json { if (!v || typeof v !== 'object' || Array.isArray(v))
>     throw Error('Expected ledger object'); return v as Json; }
> function uint(v: unknown, label: string): bigint {
>     if (typeof v === 'string' && /^\d+$/.test(v))
>         return BigInt(v);
>     if (typeof v === 'number' && Number.isSafeInteger(v) && v >= 0)
>         return BigInt(v);
>     throw Error(`Invalid or missing ${label}`);
> }
> function integer(v: unknown, label: string): number { const n = uint(v, label); if (n > BigInt(Number.MAX_SAFE_INTEGER))
>     throw Error(`Invalid ${label}`); return Number(n); }
> const max0 = (n: bigint) => n > 0n ? n : 0n;
> export async function inspectPolicy(client: Client, policy: ReinsPolicy): Promise<InspectReport> {
>     const report: InspectReport = {
>         schemaVersion: 1, complete: false,
>         snapshot: { ledgerIndex: 0, ledgerHash: '', closeTime: 0, network: policy.network },
>         accounts: { ...policy.accounts }, permissions: [],
>         exposure: { spendableDrops: null, refillDrops: null, reachableDrops: null, feeFloatDrops: null }, findings: [],
>         limitations: [
>             'Read-only XRP observations, not a universal worst-case loss bound. Agent funds are shown separately.',
>             'Network is declared by the policy, not independently established from the endpoint.',
>             'Co-signer policy limits are inactive. Toolkit observations do not add ledger enforcement.',
>             'Refills are contingent caps, not escrowed funds. Future owner funding and future ledger changes are excluded.',
>             'Refill caps use the full current vault balance conservatively because cashing Checks can release owner reserve; actual cashable funds may be lower.',
>         ],
>     };
>     const finding = (code: string, message: string) => report.findings.push({ code, message });
>     try {
>         const first = object((await client.request({ command: 'ledger', ledger_index: 'validated' })).result);
>         const ledger = object(first.ledger);
>         if (first.validated !== true)
>             throw Error('Ledger snapshot is not explicitly validated');
>         const hash = first.ledger_hash ?? ledger.ledger_hash;
>         if (typeof hash !== 'string' || !/^[A-Fa-f0-9]{64}$/.test(hash))
>             throw Error('Missing validated ledger hash');
>         report.snapshot = { ledgerHash: hash, ledgerIndex: integer(first.ledger_index ?? ledger.ledger_index, 'ledger index'), closeTime: integer(ledger.close_time, 'ledger close time'), network: policy.network };
>         async function read(req: Json): Promise<Json> {
>             const response = await client.request({ ...req, ledger_hash: hash } as never) as unknown as {
>                 result: unknown;
>             };
>             const result = object(response.result);
>             if (result.validated === false)
>                 throw Error('Response is not validated');
>             if (result.ledger_hash !== undefined && result.ledger_hash !== hash)
>                 throw Error('Response ledger hash differs from snapshot');
>             if (result.ledger_index !== undefined && integer(result.ledger_index, 'response ledger index') !== report.snapshot.ledgerIndex)
>                 throw Error('Response ledger index differs from snapshot');
>             return result;
>         }
>         async function objs(account: string): Promise<Json[]> {
>             const all: Json[] = [];
>             let marker: unknown;
>             const seen = new Set<string>();
>             do {
>                 const result = await read({ command: 'account_objects', account, limit: 400, ...marker === undefined ? {} : { marker } });
>                 if (result.account !== undefined && result.account !== account)
>                     throw Error('Object response identity differs from request');
>                 if (!Array.isArray(result.account_objects))
>                     throw Error('Missing account objects');
>                 all.push(...result.account_objects.map(object));
>                 marker = result.marker;
>                 if (marker !== undefined) {
>                     const key = JSON.stringify(marker);
>                     if (seen.has(key))
>                         throw Error('Repeated pagination marker');
>                     seen.add(key);
>                 }
>             } while (marker !== undefined);
>             return all;
>         }
>         const info: Record<string, Json> = {};
>         const objects: Record<string, Json[]> = {};
>         for (const role of ['vault', 'allowance', 'agent'] as const) {
>             const address = policy.accounts[role];
>             info[role] = object((await read({ command: 'account_info', account: address })).account_data);
>             if (info[role]!.Account !== undefined && info[role]!.Account !== address)
>                 throw Error('Account response identity differs from request');
>             objects[role] = await objs(address);
>         }
>         const fees = object((await read({ command: 'ledger_entry', index: FEES })).node);
>         if (fees.LedgerEntryType !== 'FeeSettings')
>             throw Error('Missing FeeSettings entry');
>         const base = uint(fees.ReserveBaseDrops ?? fees.ReserveBase, 'reserve base');
>         const increment = uint(fees.ReserveIncrementDrops ?? fees.ReserveIncrement, 'reserve increment');
>         const balance = (role: string) => uint(info[role]!.Balance, `${role} balance`);
>         const spendable = (role: string) => max0(balance(role) - base - increment * uint(info[role]!.OwnerCount, `${role} owner count`));
>         report.exposure.feeFloatDrops = balance('agent').toString();
>         report.exposure.spendableDrops = spendable('allowance').toString();
>         const delegates = objects.allowance!.filter(o => o.LedgerEntryType === 'Delegate');
>         const delegate = delegates.find(o => o.Authorize === policy.accounts.agent);
>         if (delegate) {
>             if (!Array.isArray(delegate.Permissions))
>                 throw Error('Missing delegated permissions');
>             report.permissions = delegate.Permissions.map(p => { const value = object(object(p).Permission).PermissionValue; if (typeof value !== 'string')
>                 throw Error('Invalid delegated permission'); return value; }).sort();
>         }
>         const want = [...policy.agent.permissions].sort();
>         report.policyMatches = !!delegate && JSON.stringify(want) === JSON.stringify(report.permissions);
>         if (!report.policyMatches)
>             finding('POLICY_DRIFT', 'Live delegation differs from policy intent; no live delegation is also a mismatch.');
>         let unknown = false;
>         const unsupported = (code: string, message: string) => { unknown = true; finding(code, message); };
>         if (policy.budget.asset !== 'XRP')
>             unsupported('ISSUED_ASSET_UNSUPPORTED', 'This first inspector reports XRP only; issued-asset exposure is unknown.');
>         if (report.permissions.some(p => !SUPPORTED.has(p)))
>             unsupported('UNSUPPORTED_PERMISSION', 'Live permissions include a type outside this inspector scope.');
>         if (report.permissions.includes('EscrowFinish'))
>             unsupported('ESCROW_EXPOSURE_UNSUPPORTED', 'EscrowFinish is delegated; reachable escrow exposure is not modelled.');
>         if (delegates.some(d => d.Authorize !== policy.accounts.agent))
>             unsupported('OTHER_DELEGATES', 'Other allowance delegates exist; authority coverage is incomplete.');
>         if (objects.vault!.some(o => o.LedgerEntryType === 'Delegate'))
>             unsupported('VAULT_DELEGATION', 'Vault delegation exists outside the supported topology.');
>         let refill = 0n;
>         const checks = new Map<string, Json>();
>         for (const role of ['vault', 'allowance', 'agent'] as const) {
>             if (info[role]!.RegularKey !== undefined)
>                 unsupported('REGULAR_KEY_COVERAGE', 'A regular key exists; this inspector does not establish alternative signing authority coverage.');
>             if (Object.keys(info[role]!).some(k => /sponsor/i.test(k)))
>                 unsupported('SPONSORSHIP_UNSUPPORTED', 'Sponsored reserve accounting is not modelled.');
>             for (const o of objects[role]!) {
>                 const type = o.LedgerEntryType;
>                 if (type === 'Check') {
>                     if (o.Account === policy.accounts.vault && o.Destination === policy.accounts.allowance) {
>                         if (typeof o.index !== 'string')
>                             throw Error('Check missing object index');
>                         checks.set(o.index, o);
>                     }
>                     else if (o.Account === policy.accounts.allowance || o.Destination === policy.accounts.allowance || o.Account === policy.accounts.agent || o.Destination === policy.accounts.agent)
>                         unsupported('OTHER_CHECK', 'A Check exists outside the modelled vault-to-allowance refill path.');
>                 }
>                 else if (type === 'Delegate') {
>                     if (role === 'agent')
>                         unsupported('AGENT_DELEGATION', 'Agent delegates authority outside the supported topology.');
>                 }
>                 else if (type === 'SignerList') {
>                     unsupported('SIGNING_TOPOLOGY_UNSUPPORTED', 'A signer list exists; this inspector does not establish its authority coverage.');
>                 }
>                 else
>                     unsupported('UNSUPPORTED_OBJECT', `Ledger object ${String(type)} is outside the XRP-only inspector scope.`);
>             }
>         }
>         for (const c of checks.values()) {
>             if (c.Expiration !== undefined && integer(c.Expiration, 'Check expiration') <= report.snapshot.closeTime)
>                 continue;
>             if (typeof c.SendMax !== 'string') {
>                 unsupported('ISSUED_CHECK_UNSUPPORTED', 'An unexpired issued-asset Check has unknown exposure.');
>                 continue;
>             }
>             refill += uint(c.SendMax, 'Check SendMax');
>         }
>         // Multiple Checks share the same vault liquidity, which is not reserved by Checks.
>         refill = refill < balance('vault') ? refill : balance('vault');
>         report.exposure.refillDrops = unknown ? null : (report.permissions.includes('CheckCash') ? refill : 0n).toString();
>         report.exposure.reachableDrops = unknown ? null : (!delegate ? 0n : (report.permissions.includes('Payment') ? spendable('allowance') + (report.permissions.includes('CheckCash') ? refill : 0n) : 0n)).toString();
>         report.complete = !unknown;
>     }
>     catch (e) {
>         finding('INSPECTION_INCOMPLETE', e instanceof Error ? e.message : 'Ledger data unavailable');
>         report.exposure.reachableDrops = null;
>         report.exposure.refillDrops = null;
>         report.complete = false;
>     }
>     return report;
> }
> ```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.