> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/onchain-accounting/code-examples.md).

# Code examples

Practical examples for reading NAV, prices, and position data from KPK funds via the NAV Calculator.

**Source:** [`INAVCalculator.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/nav/INAVCalculator.sol)

{% hint style="info" %}
Addresses below are placeholders. Use the canonical **proxy** address for your chain from [Deployment addresses](/funds/infrastructure/onchain-accounting/deployment-addresses.md), and call the proxy — never the implementation.
{% endhint %}

{% hint style="info" %}
USD values are returned with **8 decimals** (`usdDecimals()`). Pass `address(0)` as the quote asset for USD.
{% endhint %}

***

## 1. Check chain health

Before trusting a NAV reading, verify the chain is healthy — no stale prices, sequencer up.

```solidity
function healthCheck()
    external view returns (bool sequencerDown, address[] memory stalePriceAssets, address[] memory irregularPriceAssets);
```

{% hint style="info" %}
`healthCheck` returns three values, not four: it deliberately carries **no `monitorsUnhealthyPriceAssets`**, because widening its return was not affordable against the implementation's EIP-170 headroom. So an empty `irregularPriceAssets` here does not establish that the divergence check ran. Read that array off a `getAccountNav` / `getAccountNavForAdapters` result — see [Stale prices & sequencer](/funds/infrastructure/onchain-accounting/concepts/stale-prices-and-sequencer.md#when-no-monitor-answered-monitorsunhealthypriceassets).
{% endhint %}

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = parseAbi([
  "function healthCheck() view returns (bool sequencerDown, address[] stalePriceAssets, address[] irregularPriceAssets)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function checkHealth() {
  const [sequencerDown, stalePriceAssets, irregularPriceAssets] = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "healthCheck",
  });
  if (sequencerDown || stalePriceAssets.length > 0) {
    console.warn("Chain unhealthy:", { sequencerDown, stalePriceAssets });
    return false;
  }
  if (irregularPriceAssets.length > 0) {
    console.warn("Price divergence (soft alert):", { irregularPriceAssets }); // still usable — investigate
  }
  return true;
}
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = [
  "function healthCheck() view returns (bool sequencerDown, address[] stalePriceAssets, address[] irregularPriceAssets)",
];

async function checkHealth() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

  const [sequencerDown, stalePriceAssets, irregularPriceAssets] = await calc.healthCheck();
  if (sequencerDown || stalePriceAssets.length > 0) {
    console.warn("Chain unhealthy:", { sequencerDown, stalePriceAssets });
    return false;
  }
  if (irregularPriceAssets.length > 0) {
    console.warn("Price divergence (soft alert):", { irregularPriceAssets }); // still usable — investigate
  }
  return true;
}
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ABI = [{
    "name": "healthCheck", "type": "function", "stateMutability": "view",
    "inputs": [],
    "outputs": [
        {"name": "sequencerDown", "type": "bool"},
        {"name": "stalePriceAssets", "type": "address[]"},
        {"name": "irregularPriceAssets", "type": "address[]"},
    ],
}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI)
sequencer_down, stale_assets, irregular_assets = calc.functions.healthCheck().call()

if sequencer_down or stale_assets:
    print(f"Unhealthy: sequencerDown={sequencer_down}, stale={stale_assets}")
elif irregular_assets:
    print(f"Price divergence (soft alert, still usable): {irregular_assets}")
```

{% endtab %}
{% endtabs %}

***

## 2. Read NAV — single chain

Pass the account (portfolio Safe) and the quote asset (`address(0)` for USD).

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe

const ABI = parseAbi([
  `function getAccountNav(address account, address quoteAsset) view returns (
     (int256 value, (address asset, string symbol, uint8 decimals) quoteAsset,
      uint64 timestamp, (address asset, string symbol, uint8 decimals)[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale,
      (address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
      (address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets) nav
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function readNav() {
  const nav = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountNav",
    args: [ACCOUNT, zeroAddress], // zeroAddress = USD
  });

  if (nav.sequencerDown) throw new Error("L2 sequencer is down");
  if (nav.quoteAssetStale) throw new Error("Quote-asset feed stale (value fell back to USD)");
  if (nav.stalePriceAssets.length > 0) {
    throw new Error(`Stale prices for: ${nav.stalePriceAssets.map((a) => a.symbol).join(", ")}`);
  }

  // Soft signals. Read them together: an asset in the second list was NOT checked,
  // so its absence from the first list carries no information.
  if (nav.irregularPriceAssets.length > 0) {
    console.warn("Diverged:", nav.irregularPriceAssets.map((a) => a.symbol).join(", "));
  }
  if (nav.monitorsUnhealthyPriceAssets.length > 0) {
    console.warn("Unchecked (no monitor answered):",
      nav.monitorsUnhealthyPriceAssets.map((a) => a.symbol).join(", "));
  }

  console.log(`Chain NAV (USD): ${Number(nav.value) / 1e8}`);
}

readNav();
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe
const USD            = ethers.ZeroAddress;

const ABI = [
  `function getAccountNav(address account, address quoteAsset) view returns (
     tuple(int256 value, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
           uint64 timestamp, tuple(address asset, string symbol, uint8 decimals)[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale,
           tuple(address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
           tuple(address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets) nav
   )`,
];

async function readNav() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const navCalc  = new ethers.Contract(NAV_CALCULATOR, ABI, provider);
  const nav = await navCalc.getAccountNav(ACCOUNT, USD);

  if (nav.sequencerDown) throw new Error("L2 sequencer is down");
  if (nav.quoteAssetStale) throw new Error("Quote-asset feed stale");
  if (nav.stalePriceAssets.length > 0) {
    throw new Error(`Stale: ${nav.stalePriceAssets.map((a: any) => a.symbol).join(", ")}`);
  }

  // Soft signals — an asset in the second list was not checked at all.
  if (nav.irregularPriceAssets.length > 0) {
    console.warn("Diverged:", nav.irregularPriceAssets.map((a: any) => a.symbol).join(", "));
  }
  if (nav.monitorsUnhealthyPriceAssets.length > 0) {
    console.warn("Unchecked (no monitor answered):",
      nav.monitorsUnhealthyPriceAssets.map((a: any) => a.symbol).join(", "));
  }

  console.log(`Chain NAV (USD): ${Number(nav.value) / 1e8}`);
}

readNav();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."  # portfolio Safe
USD            = "0x0000000000000000000000000000000000000000"  # address(0) = USD

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
NAV_ABI = [{
    "name": "getAccountNav", "type": "function", "stateMutability": "view",
    "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
    "outputs": [{"name": "nav", "type": "tuple", "components": [
        {"name": "value",            "type": "int256"},
        {"name": "quoteAsset",       "type": "tuple", "components": ASSET},
        {"name": "timestamp",        "type": "uint64"},
        {"name": "stalePriceAssets", "type": "tuple[]", "components": ASSET},
        {"name": "sequencerDown",    "type": "bool"},
        {"name": "quoteAssetStale",  "type": "bool"},
        {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
        {"name": "quoteAssetIrregular",  "type": "bool"},
        {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
    ]}],
}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=NAV_ABI)
nav = calc.functions.getAccountNav(ACCOUNT, USD).call()

if nav[4]:           # sequencerDown
    raise RuntimeError("L2 sequencer is down")
if nav[5]:           # quoteAssetStale
    raise RuntimeError("Quote-asset feed stale")
if nav[3]:           # stalePriceAssets
    raise RuntimeError(f"Stale prices for: {[a[1] for a in nav[3]]}")
if nav[6]:           # irregularPriceAssets — soft alert, value still usable
    print(f"Price divergence (investigate): {[a[1] for a in nav[6]]}")
if nav[8]:           # monitorsUnhealthyPriceAssets — the divergence check did NOT run for these
    print(f"Unchecked, no monitor answered: {[a[1] for a in nav[8]]}")

print(f"Chain NAV (USD): {nav[0] / 1e8}")
```

{% endtab %}
{% endtabs %}

### Paging a read that no longer fits

`getAccountNav` scans **every** registered adapter in one `eth_call`. If that ever exceeds the node's gas cap you get no NAV at all — so `getAccountNavForAdapters` lets you ask the same question over a subset and reassemble.

Reassembly is where this goes wrong, so the example below is the reassembly, not the call. Three rules:

* **Only `value` adds up.** `stalePriceAssets` / `irregularPriceAssets` / `monitorsUnhealthyPriceAssets` cover only the assets *that slice* touched, and `sequencerDown` / `quoteAssetStale` / `quoteAssetIrregular` are reported per slice. Union the arrays and OR the booleans across **every** slice — checking them on one slice yields a NAV that looks healthy while silently dropping the others' warnings. Dropping the third array under-reports monitor failure, which is the falsely-reassuring direction.
* **Slice by adapter set, never by index range.** The adapter list is swap-and-pop, so a removal between calls moves the tail adapter down into a slot you already read, and it is skipped.
* **Verify with `adaptersCovered`.** Concatenate it across slices and compare against `getAllAdapters()`. A missing address means the total is short by that adapter's positions — undetectable from `value` alone, because a short NAV is a perfectly plausible number.

Pass the **same** `quoteAsset` to every slice.

{% hint style="warning" %}
`PartialNAV` places `monitorsUnhealthyPriceAssets` **after** `adaptersCovered`, whereas `NAV` ends with it. The two structs mirror each other only across their first eight fields, so a decoder written against `NAV` cannot be pointed at a `PartialNAV` past that prefix. The ordering below is `PartialNAV`'s.
{% endhint %}

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe
const ASSET_T        = "(address asset, string symbol, uint8 decimals)";

const ABI = parseAbi([
  `function getAllAdapters() view returns (address[])`,
  `function getAccountNavForAdapters(address account, address quoteAsset, address[] adapters) view returns (
     (int256 value, ${ASSET_T} quoteAsset, uint64 timestamp, ${ASSET_T}[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale, ${ASSET_T}[] irregularPriceAssets,
      bool quoteAssetIrregular, address[] adaptersCovered,
      ${ASSET_T}[] monitorsUnhealthyPriceAssets) partialNav
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function readNavPaged(sliceSize = 5) {
  // Read the adapter set ONCE, then slice that snapshot — never re-read per page.
  const all = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAllAdapters",
  });

  const slices: `0x${string}`[][] = [];
  for (let i = 0; i < all.length; i += sliceSize) slices.push([...all.slice(i, i + sliceSize)]);

  const parts = await Promise.all(slices.map((adapters) =>
    client.readContract({
      address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountNavForAdapters",
      args: [ACCOUNT, zeroAddress, adapters], // zeroAddress = USD
    })
  ));

  // value SUMS; every health signal is unioned / ORed.
  const total    = parts.reduce((acc, p) => acc + p.value, 0n);
  const stale    = new Map(parts.flatMap((p) => p.stalePriceAssets).map((a) => [a.asset, a]));
  const irregular= new Map(parts.flatMap((p) => p.irregularPriceAssets).map((a) => [a.asset, a]));
  const unchecked= new Map(parts.flatMap((p) => p.monitorsUnhealthyPriceAssets).map((a) => [a.asset, a]));
  const covered  = parts.flatMap((p) => [...p.adaptersCovered]);

  // Completeness check — a short NAV is a plausible number, so prove it isn't one.
  const missing = all.filter((a) => !covered.includes(a));
  if (missing.length) throw new Error(`NAV incomplete — adapters never read: ${missing.join(", ")}`);

  if (parts.some((p) => p.sequencerDown))   throw new Error("L2 sequencer is down");
  if (parts.some((p) => p.quoteAssetStale)) throw new Error("Quote-asset feed stale");
  if (stale.size) throw new Error(`Stale prices for: ${[...stale.values()].map((a) => a.symbol).join(", ")}`);
  if (irregular.size) console.warn(`Price divergence: ${[...irregular.values()].map((a) => a.symbol).join(", ")}`);
  if (unchecked.size) console.warn(`Unchecked, no monitor answered: ${[...unchecked.values()].map((a) => a.symbol).join(", ")}`);

  console.log(`Chain NAV (USD): ${Number(total) / 1e8}`);
}

readNavPaged();
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe
const USD            = ethers.ZeroAddress;
const ASSET_T        = "tuple(address asset, string symbol, uint8 decimals)";

const ABI = [
  `function getAllAdapters() view returns (address[])`,
  `function getAccountNavForAdapters(address account, address quoteAsset, address[] adapters) view returns (
     tuple(int256 value, ${ASSET_T} quoteAsset, uint64 timestamp, ${ASSET_T}[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale, ${ASSET_T}[] irregularPriceAssets,
           bool quoteAssetIrregular, address[] adaptersCovered,
           ${ASSET_T}[] monitorsUnhealthyPriceAssets) partialNav
   )`,
];

async function readNavPaged(sliceSize = 5) {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const navCalc  = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

  const all: string[] = [...(await navCalc.getAllAdapters())];
  const slices: string[][] = [];
  for (let i = 0; i < all.length; i += sliceSize) slices.push(all.slice(i, i + sliceSize));

  const parts = await Promise.all(
    slices.map((adapters) => navCalc.getAccountNavForAdapters(ACCOUNT, USD, adapters))
  );

  const total   = parts.reduce((acc: bigint, p: any) => acc + p.value, 0n);
  const stale   = new Map(parts.flatMap((p: any) => p.stalePriceAssets).map((a: any) => [a.asset, a]));
  const irregular = new Map(parts.flatMap((p: any) => p.irregularPriceAssets).map((a: any) => [a.asset, a]));
  const unchecked = new Map(parts.flatMap((p: any) => p.monitorsUnhealthyPriceAssets).map((a: any) => [a.asset, a]));
  const covered = parts.flatMap((p: any) => [...p.adaptersCovered]);

  const missing = all.filter((a) => !covered.includes(a));
  if (missing.length) throw new Error(`NAV incomplete — adapters never read: ${missing.join(", ")}`);

  if (parts.some((p: any) => p.sequencerDown))   throw new Error("L2 sequencer is down");
  if (parts.some((p: any) => p.quoteAssetStale)) throw new Error("Quote-asset feed stale");
  if (stale.size) throw new Error(`Stale: ${[...stale.values()].map((a: any) => a.symbol).join(", ")}`);
  if (irregular.size) console.warn(`Price divergence: ${[...irregular.values()].map((a: any) => a.symbol).join(", ")}`);
  if (unchecked.size) console.warn(`Unchecked, no monitor answered: ${[...unchecked.values()].map((a: any) => a.symbol).join(", ")}`);

  console.log(`Chain NAV (USD): ${Number(total) / 1e8}`);
}

readNavPaged();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."  # portfolio Safe
USD            = "0x0000000000000000000000000000000000000000"
SLICE_SIZE     = 5

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
ABI = [
    {"name": "getAllAdapters", "type": "function", "stateMutability": "view",
     "inputs": [], "outputs": [{"name": "adapters", "type": "address[]"}]},
    {"name": "getAccountNavForAdapters", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"},
                {"name": "adapters", "type": "address[]"}],
     "outputs": [{"name": "partialNav", "type": "tuple", "components": [
         {"name": "value",                "type": "int256"},
         {"name": "quoteAsset",           "type": "tuple", "components": ASSET},
         {"name": "timestamp",            "type": "uint64"},
         {"name": "stalePriceAssets",     "type": "tuple[]", "components": ASSET},
         {"name": "sequencerDown",        "type": "bool"},
         {"name": "quoteAssetStale",      "type": "bool"},
         {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
         {"name": "quoteAssetIrregular",  "type": "bool"},
         {"name": "adaptersCovered",      "type": "address[]"},
         # PartialNAV appends this AFTER adaptersCovered — unlike NAV, where it is last.
         {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
     ]}]},
]

w3   = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI)

# Snapshot the adapter set once, then slice it — never re-read between pages.
all_adapters = calc.functions.getAllAdapters().call()
slices = [all_adapters[i:i + SLICE_SIZE] for i in range(0, len(all_adapters), SLICE_SIZE)]
parts  = [calc.functions.getAccountNavForAdapters(ACCOUNT, USD, s).call() for s in slices]

total      = sum(p[0] for p in parts)                        # only value adds up
stale      = {a[0]: a for p in parts for a in p[3]}          # union, deduped by address
irregular  = {a[0]: a for p in parts for a in p[6]}
covered    = [a for p in parts for a in p[8]]                # adaptersCovered
unchecked  = {a[0]: a for p in parts for a in p[9]}          # monitorsUnhealthyPriceAssets

missing = [a for a in all_adapters if a not in covered]
if missing:
    raise RuntimeError(f"NAV incomplete — adapters never read: {missing}")

if any(p[4] for p in parts):                                 # sequencerDown, ORed
    raise RuntimeError("L2 sequencer is down")
if any(p[5] for p in parts):                                 # quoteAssetStale, ORed
    raise RuntimeError("Quote-asset feed stale")
if stale:
    raise RuntimeError(f"Stale prices for: {[a[1] for a in stale.values()]}")
if irregular:
    print(f"Price divergence (investigate): {[a[1] for a in irregular.values()]}")
if unchecked:
    print(f"Unchecked, no monitor answered: {[a[1] for a in unchecked.values()]}")

print(f"Chain NAV (USD): {total / 1e8}")
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
A slice is rejected rather than returning a quietly wrong `value`: `EmptyAdapterSlice()`, `AdapterNotRegistered(address)` for anything absent from `getAllAdapters()`, and `DuplicateAdapterInSlice(address)` for a repeat. A **meta-adapter is atomic** — naming one covers all its governed instances, and there is no paging within a single adapter, so pagination cannot rescue one that is oversized on its own.
{% endhint %}

***

## 3. Read global NAV — all chains

Funds deployed on multiple chains: read each chain and sum the per-chain `value` once every chain is healthy.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet, arbitrum, base } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // same proxy address on every chain
const ACCOUNT        = "0x...";

const ABI = parseAbi([
  `function getAccountNav(address account, address quoteAsset) view returns (
     (int256 value, (address asset, string symbol, uint8 decimals) quoteAsset,
      uint64 timestamp, (address asset, string symbol, uint8 decimals)[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale,
      (address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
      (address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets))`,
]);

const CHAINS = [
  { chain: mainnet,  rpc: "https://eth.llamarpc.com" },
  { chain: arbitrum, rpc: "https://arb1.arbitrum.io/rpc" },
  { chain: base,     rpc: "https://mainnet.base.org" },
];

async function readGlobalNav() {
  const results = await Promise.all(
    CHAINS.map(({ chain, rpc }) =>
      createPublicClient({ chain, transport: http(rpc) }).readContract({
        address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountNav",
        args: [ACCOUNT, zeroAddress],
      })
    )
  );

  results.forEach((nav, i) => {
    if (nav.sequencerDown || nav.quoteAssetStale || nav.stalePriceAssets.length > 0) {
      throw new Error(`Chain ${CHAINS[i].chain.name} is unhealthy`);
    }
  });

  const globalNav = results.reduce((sum, nav) => sum + nav.value, 0n);
  console.log(`Global NAV (USD): ${Number(globalNav) / 1e8}`);
  return globalNav;
}

readGlobalNav();
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // same proxy address on every chain
const ACCOUNT        = "0x...";
const USD            = ethers.ZeroAddress;

const ABI = [
  `function getAccountNav(address account, address quoteAsset) view returns (
     tuple(int256 value, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
           uint64 timestamp, tuple(address asset, string symbol, uint8 decimals)[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale,
           tuple(address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
           tuple(address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets))`,
];

const RPCS = {
  mainnet:  "https://eth.llamarpc.com",
  arbitrum: "https://arb1.arbitrum.io/rpc",
  base:     "https://mainnet.base.org",
};

async function readGlobalNav() {
  const entries = Object.entries(RPCS);
  const results = await Promise.all(
    entries.map(([, rpc]) => {
      const provider = new ethers.JsonRpcProvider(rpc);
      return new ethers.Contract(NAV_CALCULATOR, ABI, provider).getAccountNav(ACCOUNT, USD);
    })
  );

  results.forEach((nav, i) => {
    if (nav.sequencerDown || nav.quoteAssetStale || nav.stalePriceAssets.length > 0) {
      throw new Error(`Chain ${entries[i][0]} is unhealthy`);
    }
  });

  const globalNav = results.reduce((sum, nav) => sum + nav.value, 0n);
  console.log(`Global NAV (USD): ${Number(globalNav) / 1e8}`);
  return globalNav;
}

readGlobalNav();
```

{% endtab %}

{% tab title="Python" %}

```python
import asyncio
from web3 import AsyncWeb3

NAV_CALCULATOR = "0x..."  # same proxy address on every chain
ACCOUNT        = "0x..."
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
ABI = [{"name": "getAccountNav", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple", "components": [
            {"name": "value", "type": "int256"},
            {"name": "quoteAsset", "type": "tuple", "components": ASSET},
            {"name": "timestamp", "type": "uint64"},
            {"name": "stalePriceAssets", "type": "tuple[]", "components": ASSET},
            {"name": "sequencerDown", "type": "bool"},
            {"name": "quoteAssetStale", "type": "bool"},
            {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
            {"name": "quoteAssetIrregular", "type": "bool"},
            {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
        ]}]}]

RPCS = {
    "mainnet":  "https://eth.llamarpc.com",
    "arbitrum": "https://arb1.arbitrum.io/rpc",
    "base":     "https://mainnet.base.org",
}

async def read_chain(chain, rpc):
    w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(rpc))
    nav = await w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getAccountNav(ACCOUNT, USD).call()
    return chain, nav

async def read_global_nav():
    results = await asyncio.gather(*[read_chain(c, r) for c, r in RPCS.items()])
    for chain, nav in results:
        if nav[4] or nav[5] or nav[3]:
            raise RuntimeError(f"{chain} is unhealthy")
    total = sum(nav[0] for _, nav in results)
    print(f"Global NAV (USD): {total / 1e8}")
    return total

asyncio.run(read_global_nav())
```

{% endtab %}
{% endtabs %}

***

## 4. Enumerate positions with labels

`getAccountPositions` returns the typed positions. For human-readable labels in one call, use the NAV Calculator's verbose read `getAccountPositionsVerbose`, which adds `protocolName` and a `labels` array (the breadcrumb each adapter exposes through `IPositionDescribable`). There is no separate lens contract — call the NAV Calculator proxy directly.

{% tabs %}
{% tab title="viem (verbose)" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT  = "0x...";

const ABI = parseAbi([
  `function getAccountPositionsVerbose(address account, address quoteAsset) view returns (
     (
       (
         (address asset, string symbol, uint8 decimals) balanceAsset,
         address balanceAdapter, uint256 amount, bool isDebt,
         (address asset, string symbol, uint8 decimals) quoteAsset,
         int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
         bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId, bool irregular, bool quoteAssetIrregular
       ) position,
       string protocolName,
       string[] labels
     )[]
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function getPortfolio() {
  const rows = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountPositionsVerbose",
    args: [ACCOUNT, zeroAddress],
  });

  for (const { position: p, protocolName, labels } of rows) {
    const usd = Number(p.value) / 1e8;
    const detail = [protocolName, ...labels].filter(Boolean).join(" — ");
    const label = detail || p.balanceAsset.symbol;
    console.log(`${label}: $${usd.toFixed(2)}${p.stale ? " ⚠ stale" : ""}`);
  }
}

getPortfolio();
```

{% endtab %}

{% tab title="ethers.js (verbose)" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT  = "0x...";
const USD      = ethers.ZeroAddress;

const ABI = [
  `function getAccountPositionsVerbose(address account, address quoteAsset) view returns (
     tuple(
       tuple(
         tuple(address asset, string symbol, uint8 decimals) balanceAsset,
         address balanceAdapter, uint256 amount, bool isDebt,
         tuple(address asset, string symbol, uint8 decimals) quoteAsset,
         int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
         bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId, bool irregular, bool quoteAssetIrregular
       ) position,
       string protocolName,
       string[] labels
     )[]
   )`,
];

async function getPortfolio() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);
  const rows = await calc.getAccountPositionsVerbose(ACCOUNT, USD);

  for (const { position: p, protocolName, labels } of rows) {
    const usd = Number(p.value) / 1e8;
    const detail = [protocolName, ...labels].filter(Boolean).join(" — ");
    const label = detail || p.balanceAsset.symbol;
    console.log(`${label}: $${usd.toFixed(2)}${p.stale ? " ⚠ stale" : ""}`);
  }
}

getPortfolio();
```

{% endtab %}

{% tab title="Python (typed)" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
POS = [
    {"name": "balanceAsset",   "type": "tuple", "components": ASSET},
    {"name": "balanceAdapter", "type": "address"},
    {"name": "amount",         "type": "uint256"},
    {"name": "isDebt",         "type": "bool"},
    {"name": "quoteAsset",     "type": "tuple", "components": ASSET},
    {"name": "value",          "type": "int256"},
    {"name": "price",          "type": "int256"},
    {"name": "priceDecimals",  "type": "uint8"},
    {"name": "stale",          "type": "bool"},
    {"name": "quoteAssetStale","type": "bool"},
    {"name": "protocolBrand",      "type": "bytes32"},
    {"name": "protocolId",         "type": "bytes32"},
    {"name": "protocolSubId",      "type": "bytes32"},
    {"name": "positionId",         "type": "bytes"},
    {"name": "positionKind",       "type": "uint8"},
    {"name": "isLocked",           "type": "bool"},
    {"name": "positionInstanceId", "type": "bytes32"},
    {"name": "irregular",          "type": "bool"},
    {"name": "quoteAssetIrregular","type": "bool"},
]
ABI = [{"name": "getAccountPositions", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple[]", "components": POS}]}]

KIND = ["Wallet", "Supplied", "Borrowed", "Collateral", "Staking", "Rewards", "Fees"]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
positions = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getAccountPositions(ACCOUNT, USD).call()

for p in positions:
    usd = p[5] / 1e8                       # value
    stale = " ⚠ stale" if p[8] else ""
    print(f"{p[0][1]} [{KIND[p[14]]}]: ${usd:,.2f}{stale}")   # positionKind
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Position **identity** is the typed key `protocolSubId` + `positionId` + `balanceAsset.asset` + `positionKind` (per chain). The broader `protocolBrand` / `protocolId` are grouping layers; `positionInstanceId` is an ephemeral per-item handle and `isLocked` is a mutable per-leg attribute — both excluded from the key (so claimable/locked and per-item legs sum). See [Position identity](/funds/infrastructure/onchain-accounting/concepts/position-identity.md) for decode schemas and the reconciliation key. The `protocolName` string and `labels[]` array from the verbose reads are display-only.
{% endhint %}

***

## 5. Read the latest price for an asset

Each asset is priced by its primary feed. Read it with `getPriceData(asset)`, which returns the full `PriceFeedData` snapshot — `price`, `decimals`, `stale`, plus the primary feed's type, heartbeat, `updatedAt`, `healthyFeedCount`, and the divergence signal (`irregular`, `divergenceBps`, `monitorFeedCount`).

For a **Custom** (composite) feed, `updatedAt` and `chainlinkHeartbeat` report the [governing leg](/funds/infrastructure/onchain-accounting/price-feeds.md#governing-leg-freshness) — the underlying Chainlink leg closest to its own staleness deadline — rather than the registered config heartbeat. Treat that pair as advisory and gate on `stale` / `sequencerDown`.

```solidity
function getPriceData(address underlyingAsset)
    external view returns (PriceFeedData memory priceFeedData);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = parseAbi([
  `function getPriceData(address token) view returns (
     (address priceFeed, uint8 priceType, int256 price, uint8 decimals,
      uint256 chainlinkHeartbeat, uint256 updatedAt, bool stale, bool sequencerDown,
      uint256 healthyFeedCount, bool irregular, uint256 divergenceBps, uint256 monitorFeedCount)
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const d = await client.readContract({
  address: NAV_CALCULATOR, abi: ABI, functionName: "getPriceData", args: [WETH],
});

if (d.stale) console.warn("Price is stale");
console.log(`WETH price: $${(Number(d.price) / 10 ** d.decimals).toFixed(2)}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = [
  `function getPriceData(address token) view returns (
     tuple(address priceFeed, uint8 priceType, int256 price, uint8 decimals,
           uint256 chainlinkHeartbeat, uint256 updatedAt, bool stale, bool sequencerDown,
           uint256 healthyFeedCount, bool irregular, uint256 divergenceBps, uint256 monitorFeedCount)
   )`,
];

async function readPrice() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

  const d = await calc.getPriceData(WETH);
  if (d.stale) console.warn("Price is stale");
  console.log(`WETH price: $${(Number(d.price) / 10 ** Number(d.decimals)).toFixed(2)}`);
}

readPrice();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"

PRICE_FEED_DATA = [
    {"name": "priceFeed",          "type": "address"},
    {"name": "priceType",          "type": "uint8"},
    {"name": "price",              "type": "int256"},
    {"name": "decimals",           "type": "uint8"},
    {"name": "chainlinkHeartbeat", "type": "uint256"},
    {"name": "updatedAt",          "type": "uint256"},
    {"name": "stale",              "type": "bool"},
    {"name": "sequencerDown",      "type": "bool"},
    {"name": "healthyFeedCount",   "type": "uint256"},
    {"name": "irregular",          "type": "bool"},
    {"name": "divergenceBps",      "type": "uint256"},
    {"name": "monitorFeedCount",   "type": "uint256"},
]
ABI = [{"name": "getPriceData", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "token", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple", "components": PRICE_FEED_DATA}]}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
d = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getPriceData(WETH).call()
price, decimals, stale = d[2], d[3], d[6]
if stale:
    print("Warning: stale")
print(f"WETH: ${price / 10**decimals:,.2f}")
```

{% endtab %}
{% endtabs %}

***

## 6. Value a basket of (asset, amount) pairs

`calculateValue` prices an arbitrary set of `(asset, amount)` pairs in one call — handy for valuing assets you already know off-chain (e.g. the inputs of a pending subscription) without enumerating positions. Returns the total in the quote currency (USD = 8 decimals) and a `quoteAssetStale` flag.

```solidity
function calculateValue(address[] assets, uint256[] amounts, address quoteAsset)
    external view returns (int256 value, bool quoteAssetStale);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const ABI = parseAbi([
  "function calculateValue(address[] assets, uint256[] amounts, address quoteAsset) view returns (int256 value, bool quoteAssetStale)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const [value, quoteAssetStale] = await client.readContract({
  address: NAV_CALCULATOR, abi: ABI, functionName: "calculateValue",
  args: [[WETH, USDC], [10n ** 18n, 5_000n * 10n ** 6n], zeroAddress], // 1 WETH + 5,000 USDC, in USD
});

if (quoteAssetStale) console.warn("Quote-asset feed stale (value fell back to USD)");
console.log(`Basket value (USD): ${Number(value) / 1e8}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const ABI = [
  "function calculateValue(address[] assets, uint256[] amounts, address quoteAsset) view returns (int256 value, bool quoteAssetStale)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

const [value, quoteAssetStale] = await calc.calculateValue(
  [WETH, USDC], [10n ** 18n, 5_000n * 10n ** 6n], ethers.ZeroAddress,
);
if (quoteAssetStale) console.warn("Quote-asset feed stale");
console.log(`Basket value (USD): ${Number(value) / 1e8}`);
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
USD  = "0x0000000000000000000000000000000000000000"

ABI = [{"name": "calculateValue", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "assets", "type": "address[]"}, {"name": "amounts", "type": "uint256[]"},
                   {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "value", "type": "int256"}, {"name": "quoteAssetStale", "type": "bool"}]}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
value, quote_stale = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.calculateValue(
    [WETH, USDC], [10**18, 5_000 * 10**6], USD).call()

if quote_stale:
    print("Warning: quote-asset feed stale")
print(f"Basket value (USD): {value / 1e8}")
```

{% endtab %}
{% endtabs %}

***

## 7. Decode position identity & group by protocol

Each position carries the 3-level protocol taxonomy plus `positionId` + `positionKind` + `isLocked` (see [Position identity](/funds/infrastructure/onchain-accounting/concepts/position-identity.md)). The identity-key level is **`protocolSubId`** (`keccak256` of the product slug); `positionId` is `abi.encode(...)` whose decode schema is fixed by that slug. This example maps each position back to its product, decodes the coordinate, totals value per product, and builds the stable reconciliation key.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress, keccak256, toHex, decodeAbiParameters, encodeAbiParameters } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";

const ABI = parseAbi([
  `function getAccountPositions(address account, address quoteAsset) view returns (
     ((address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, (address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
]);

// protocolSubId = keccak256(product slug) — the identity-key level. Build the reverse lookup for the slugs you care about.
const SLUGS = ["wallet", "aave-v3-lending", "spark-lend", "morpho-vaults", "morpho-markets",
  "compound-v3-comets", "balancer-v2-pools", "balancer-v3-pools", "curve-pools", "convex-curve-lp-staking",
  "uniswap-v3", "stakewise-v3-vaults", "nexus-mutual-staking-pools", "stakewise-v3-exit"];
const SLUG_BY_ID = new Map(SLUGS.map((s) => [keccak256(toHex(s)), s]));

// positionId decode schema, keyed by protocolSubId (see Position identity for the full table)
function decodeId(slug: string, positionId: `0x${string}`) {
  if (positionId === "0x") return null;                                   // defensive: empty id
  if (["morpho-markets", "balancer-v2-pools"].includes(slug)) return decodeAbiParameters([{ type: "bytes32" }], positionId)[0];
  return decodeAbiParameters([{ type: "address" }], positionId)[0];      // default: a single address (pool / vault / queue anchor)
}

const client = createPublicClient({ chain: mainnet, transport: http() });
const chainId = BigInt(client.chain.id);
const positions = await client.readContract({
  address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountPositions", args: [ACCOUNT, zeroAddress],
});

const byProtocol = new Map<string, bigint>();
for (const p of positions) {
  const slug = SLUG_BY_ID.get(p.protocolSubId) ?? p.protocolSubId;      // reconcile on protocolSubId (not protocolId)
  const coord = decodeId(slug, p.positionId);
  byProtocol.set(slug, (byProtocol.get(slug) ?? 0n) + p.value);
  // stable key across reads & adapter redeploys (excludes protocolBrand/protocolId, balanceAdapter, positionInstanceId, isLocked, amount, pricing)
  const key = keccak256(encodeAbiParameters(
    [{ type: "uint256" }, { type: "bytes32" }, { type: "bytes" }, { type: "address" }, { type: "uint8" }],
    [chainId, p.protocolSubId, p.positionId, p.balanceAsset.asset, p.positionKind],
  ));
  console.log(`${slug} ${coord ?? ""} — $${(Number(p.value) / 1e8).toFixed(2)}  key=${key.slice(0, 10)}…`);
}
for (const [slug, total] of byProtocol) console.log(`Σ ${slug}: $${(Number(total) / 1e8).toFixed(2)}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";

const ABI = [
  `function getAccountPositions(address account, address quoteAsset) view returns (
     tuple(tuple(address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
];

const SLUGS = ["wallet", "aave-v3-lending", "spark-lend", "morpho-vaults", "morpho-markets",
  "compound-v3-comets", "balancer-v2-pools", "balancer-v3-pools", "curve-pools", "convex-curve-lp-staking",
  "uniswap-v3", "stakewise-v3-vaults", "nexus-mutual-staking-pools", "stakewise-v3-exit"];
const SLUG_BY_ID = new Map(SLUGS.map((s) => [ethers.id(s), s]));        // ethers.id = keccak256(utf8)
const abi = ethers.AbiCoder.defaultAbiCoder();

function decodeId(slug: string, positionId: string) {
  if (positionId === "0x") return null;                                   // defensive: empty id
  if (["morpho-markets", "balancer-v2-pools"].includes(slug)) return abi.decode(["bytes32"], positionId)[0];
  return abi.decode(["address"], positionId)[0];                          // default: a single address (pool / vault / queue anchor)
}

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const chainId = (await provider.getNetwork()).chainId;
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);
const positions = await calc.getAccountPositions(ACCOUNT, ethers.ZeroAddress);

const byProtocol = new Map<string, bigint>();
for (const p of positions) {
  const slug = SLUG_BY_ID.get(p.protocolSubId) ?? p.protocolSubId;      // reconcile on protocolSubId (not protocolId)
  const coord = decodeId(slug, p.positionId);
  byProtocol.set(slug, (byProtocol.get(slug) ?? 0n) + p.value);
  const key = ethers.keccak256(abi.encode(
    ["uint256", "bytes32", "bytes", "address", "uint8"],
    [chainId, p.protocolSubId, p.positionId, p.balanceAsset.asset, p.positionKind],
  ));
  console.log(`${slug} ${coord ?? ""} — $${(Number(p.value) / 1e8).toFixed(2)}  key=${key.slice(0, 10)}…`);
}
for (const [slug, total] of byProtocol) console.log(`Σ ${slug}: $${(Number(total) / 1e8).toFixed(2)}`);
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3
from eth_abi import decode, encode

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
POS = [
    {"name": "balanceAsset", "type": "tuple", "components": ASSET}, {"name": "balanceAdapter", "type": "address"},
    {"name": "amount", "type": "uint256"}, {"name": "isDebt", "type": "bool"},
    {"name": "quoteAsset", "type": "tuple", "components": ASSET}, {"name": "value", "type": "int256"},
    {"name": "price", "type": "int256"}, {"name": "priceDecimals", "type": "uint8"},
    {"name": "stale", "type": "bool"}, {"name": "quoteAssetStale", "type": "bool"},
    {"name": "protocolBrand", "type": "bytes32"}, {"name": "protocolId", "type": "bytes32"},
    {"name": "protocolSubId", "type": "bytes32"}, {"name": "positionId", "type": "bytes"},
    {"name": "positionKind", "type": "uint8"},
    {"name": "isLocked", "type": "bool"}, {"name": "positionInstanceId", "type": "bytes32"},
]
ABI = [{"name": "getAccountPositions", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple[]", "components": POS}]}]

SLUGS = ["wallet", "aave-v3-lending", "spark-lend", "morpho-vaults", "morpho-markets",
         "compound-v3-comets", "balancer-v2-pools", "balancer-v3-pools", "curve-pools", "convex-curve-lp-staking",
         "uniswap-v3", "stakewise-v3-vaults", "nexus-mutual-staking-pools", "stakewise-v3-exit"]
SLUG_BY_ID = {bytes(Web3.keccak(text=s)): s for s in SLUGS}

def decode_id(slug, pid):                          # pid is bytes (the bytes positionId)
    if not pid:
        return None                                 # defensive: empty id
    if slug in ("morpho-markets", "balancer-v2-pools"):
        return "0x" + decode(["bytes32"], pid)[0].hex()
    return decode(["address"], pid)[0]              # default: a single address (pool / vault / queue anchor)

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
chain_id = w3.eth.chain_id
positions = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getAccountPositions(ACCOUNT, USD).call()

by_protocol = {}
for p in positions:
    slug = SLUG_BY_ID.get(bytes(p[12]), "0x" + p[12].hex())     # protocolSubId (the identity-key level)
    coord = decode_id(slug, p[13])                              # positionId
    by_protocol[slug] = by_protocol.get(slug, 0) + p[5]         # value
    key = Web3.keccak(encode(
        ["uint256", "bytes32", "bytes", "address", "uint8"],
        [chain_id, p[12], p[13], p[0][0], p[14]],               # chainId, protocolSubId, positionId, asset, positionKind
    ))
    print(f"{slug} {coord if coord is not None else ''} — ${p[5] / 1e8:,.2f}  key={key.hex()[:10]}…")

for slug, total in by_protocol.items():
    print(f"Σ {slug}: ${total / 1e8:,.2f}")
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The slug list above is illustrative — the full `protocolSubId` catalogue and every `positionId` decode schema are in [Position identity](/funds/infrastructure/onchain-accounting/concepts/position-identity.md). The reconciliation key deliberately excludes `protocolBrand`/`protocolId` (grouping only), `positionInstanceId` (ephemeral), `isLocked` (mutable per-leg), `balanceAdapter`, `amount`, and pricing, so it stays stable across reads and adapter redeploys.
{% endhint %}

***

## 8. Single-asset exposure

`hasPositions` is a cheap pre-check before enumerating; `getAssetsWithPositions` lists the assets an account holds something in; `getAccountPositionsForAsset` returns the positions for one asset across every adapter that reports it (e.g. all WETH exposure — wallet, supplied, staked).

```solidity
function hasPositions(address account) external view returns (bool);
function getAssetsWithPositions(address account) external view returns (address[] memory);
function getAccountPositionsForAsset(address account, address asset, address quoteAsset)
    external view returns (Position[] memory);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = parseAbi([
  "function hasPositions(address account) view returns (bool)",
  `function getAccountPositionsForAsset(address account, address asset, address quoteAsset) view returns (
     ((address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, (address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

if (!(await client.readContract({ address: NAV_CALCULATOR, abi: ABI, functionName: "hasPositions", args: [ACCOUNT] }))) {
  console.log("No positions");
} else {
  const rows = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountPositionsForAsset",
    args: [ACCOUNT, WETH, zeroAddress],
  });
  const total = rows.reduce((s, p) => s + p.value, 0n);
  console.log(`WETH exposure across ${rows.length} positions: $${Number(total) / 1e8}`);
}
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = [
  "function hasPositions(address account) view returns (bool)",
  `function getAccountPositionsForAsset(address account, address asset, address quoteAsset) view returns (
     tuple(tuple(address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

if (!(await calc.hasPositions(ACCOUNT))) {
  console.log("No positions");
} else {
  const rows = await calc.getAccountPositionsForAsset(ACCOUNT, WETH, ethers.ZeroAddress);
  const total = rows.reduce((s: bigint, p: any) => s + p.value, 0n);
  console.log(`WETH exposure across ${rows.length} positions: $${Number(total) / 1e8}`);
}
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."
WETH           = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
POS = [
    {"name": "balanceAsset", "type": "tuple", "components": ASSET}, {"name": "balanceAdapter", "type": "address"},
    {"name": "amount", "type": "uint256"}, {"name": "isDebt", "type": "bool"},
    {"name": "quoteAsset", "type": "tuple", "components": ASSET}, {"name": "value", "type": "int256"},
    {"name": "price", "type": "int256"}, {"name": "priceDecimals", "type": "uint8"},
    {"name": "stale", "type": "bool"}, {"name": "quoteAssetStale", "type": "bool"},
    {"name": "protocolBrand", "type": "bytes32"}, {"name": "protocolId", "type": "bytes32"},
    {"name": "protocolSubId", "type": "bytes32"}, {"name": "positionId", "type": "bytes"},
    {"name": "positionKind", "type": "uint8"},
    {"name": "isLocked", "type": "bool"}, {"name": "positionInstanceId", "type": "bytes32"},
]
ABI = [
    {"name": "hasPositions", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "account", "type": "address"}], "outputs": [{"type": "bool"}]},
    {"name": "getAccountPositionsForAsset", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "account", "type": "address"}, {"name": "asset", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
     "outputs": [{"name": "", "type": "tuple[]", "components": POS}]},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI)

if not calc.functions.hasPositions(ACCOUNT).call():
    print("No positions")
else:
    rows = calc.functions.getAccountPositionsForAsset(ACCOUNT, WETH, USD).call()
    total = sum(p[5] for p in rows)            # value
    print(f"WETH exposure across {len(rows)} positions: ${total / 1e8:,.2f}")
```

{% endtab %}
{% endtabs %}

***

## 9. List the assets a fund tracks

`getRegisteredAssets` returns every asset registered on the chain's NAV Calculator (with symbol and decimals); `isAssetRegistered` checks a single one. Useful to discover coverage before pricing or to validate an integration.

```solidity
function getRegisteredAssets() external view returns (Asset[] memory); // Asset = (address, string symbol, uint8 decimals)
function isAssetRegistered(address asset) external view returns (bool);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = parseAbi([
  "function getRegisteredAssets() view returns ((address asset, string symbol, uint8 decimals)[])",
  "function isAssetRegistered(address asset) view returns (bool)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const assets = await client.readContract({ address: NAV_CALCULATOR, abi: ABI, functionName: "getRegisteredAssets" });
console.log(`${assets.length} registered assets:`);
for (const a of assets) console.log(`  ${a.symbol} (${a.decimals} dp) — ${a.asset}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = [
  "function getRegisteredAssets() view returns (tuple(address asset, string symbol, uint8 decimals)[])",
  "function isAssetRegistered(address asset) view returns (bool)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

const assets = await calc.getRegisteredAssets();
console.log(`${assets.length} registered assets:`);
for (const a of assets) console.log(`  ${a.symbol} (${a.decimals} dp) — ${a.asset}`);
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
ABI = [
    {"name": "getRegisteredAssets", "type": "function", "stateMutability": "view",
     "inputs": [], "outputs": [{"name": "", "type": "tuple[]", "components": ASSET}]},
    {"name": "isAssetRegistered", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "asset", "type": "address"}], "outputs": [{"type": "bool"}]},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
assets = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getRegisteredAssets().call()
print(f"{len(assets)} registered assets:")
for a in assets:
    print(f"  {a[1]} ({a[2]} dp) — {a[0]}")
```

{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/onchain-accounting/code-examples.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
