> 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/concepts/asset-classification.md).

# Asset classification

Every registered asset is classified on **two orthogonal axes**, kept separate because no single category answers both questions:

* **`PegKind`** — what the price *tracks*. `Pegged` or `Floating`.
* **`AccrualKind`** — how value *accrues*. `None`, `Rebasing`, or `Appreciating`.

{% hint style="warning" %}
**The registry lives at `0xf60467a88C2b4d0f46C0c41C3Ba0C74da3d3CCD0` — the same address on all 24 chains** (CREATE2; it carried 14 different addresses before 2026-08-19, so discard any older per-chain value). Whether it is deployed on a given chain remains a deployment question, and this page is not its authority: [Deployment addresses](/funds/infrastructure/onchain-accounting/deployment-addresses.md) lists every live contract: if no `AssetKindRegistry` appears there for a chain, none is deployed on it and there is no address to call. What follows is what the contract implements wherever it is deployed.
{% endhint %}

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

***

## The two axes

`PegKind` means pegged **to the quote asset** — USD for the default `quoteAsset == address(0)` read — not "pegged to something in general". So USDC, USDT, DAI and GHO are `Pegged`, while **wstETH is `Floating`** even though it is rigidly related to stETH: it does not track USD.

| `AccrualKind`  | What grows                                  | Examples                                   |
| -------------- | ------------------------------------------- | ------------------------------------------ |
| `None`         | Neither balance nor rate                    | WETH, USDC, AAVE, WBTC                     |
| `Rebasing`     | The **balance** grows; unit price stays put | stETH, eETH, aTokens                       |
| `Appreciating` | The **rate** grows; balance stays static    | wstETH, sDAI, sGHO, sUSDe, ERC-4626 shares |

Across the seven configured chains, **93 assets** carry an accrual classification: **63 `None`**, **27 `Appreciating`**, **3 `Rebasing`**.

### Why two axes rather than one category

A single flat enum forces a wrong answer on assets already registered here, because the two properties genuinely cross:

|                | `None` | `Rebasing`                                 | `Appreciating`                     |
| -------------- | ------ | ------------------------------------------ | ---------------------------------- |
| **`Pegged`**   | USDC   | *(an aToken — none registered; see below)* | *(cannot be asserted — see below)* |
| **`Floating`** | WETH   | stETH                                      | wstETH, sDAI, sGHO, sUSDe, sUSDS   |

stETH and wstETH differ on the accrual axis **only** — both are `Floating`. Collapse the axes and that distinction is unexpressible.

{% hint style="info" %}
**`sDAI` is `Floating`, not `Pegged`** — a yield-bearing wrapper has no peg. Its USD price is not `$1` and rises with accrued yield, for the same reason wstETH is `Floating`. `Appreciating` is in fact the mechanism that makes an asset unpeggable: a growing redemption rate on a static balance moves the price off `$1`.
{% endhint %}

***

## The peg axis is derived, not stored

`PegKind` is **not stored anywhere**. It is computed on read from the NAV's per-asset `pegToleranceBps` — the switch that enables the [`$1` peg guard](/funds/infrastructure/onchain-accounting/price-feeds.md#the-divergence-and-irregular-signal):

```
pegKind(asset) = pegToleranceBps(asset) > 0 ? Pegged : Floating
```

There is exactly **one authority** for whether an asset is pegged, so there is no second copy to fall out of step and no cross-check needed to police one. Of the 93 classified assets, **25** carry a non-zero peg tolerance and therefore read as `Pegged`.

Because the value is read live rather than snapshotted at classification time, retuning a tolerance is immediately reflected.

{% hint style="warning" %}
**`pegToleranceBps` now does two jobs — read this before zeroing one.** It is both a *threshold* ("flag drift beyond this") and a *switch* ("this asset should be worth `$1`"). Retuning a non-zero threshold — say `100` → `250` — leaves the classification untouched; **it is crossing zero that flips it.** The two jobs are independent everywhere except at that boundary.

The trap lives exactly there: zeroing the tolerance on a genuine stablecoin for an *operational* reason — a feed too noisy to guard, say — silently reclassifies that asset as `Floating`. Nothing reverts and nothing warns, and every off-chain consumer stops believing it is a stablecoin. Disabling the `$1` guard on an asset that really is pegged needs its own field, not a zeroed tolerance.
{% endhint %}

### The two empty cells are empty for different reasons

* **`Pegged + Rebasing` is real and reachable.** An aToken for USDC has a growing balance at a `$1` price. No registered asset occupies the cell, which makes it a forward-compatibility slot rather than a contradiction.
* **`Pegged + Appreciating` cannot be&#x20;*****asserted*****.** Nothing writes a peg kind, so it cannot be declared for an asset. It remains **reachable as a read**: set a non-zero peg tolerance on an asset stored as `Appreciating` and the pair reads back `Pegged + Appreciating`, because the peg is computed from that tolerance. That is a true report of the configuration, not an inconsistency to be caught.

{% hint style="info" %}
**Do not use `PegKind` as a staleness or safety oracle.** It reports what an asset's price is *supposed* to track. It says nothing about whether the current price is fresh, or whether the asset is holding its peg right now — those are [`stalePriceAssets` and the `irregular` signal](/funds/infrastructure/onchain-accounting/concepts/stale-prices-and-sequencer.md).
{% endhint %}

***

## Reading an asset's description

**One call on the NAV Calculator returns everything public about an asset** — both classification axes and its [display labels](#display-labels). A consumer needs only the NAV address it already holds, and never has to learn that a registry exists:

```solidity
// On NAVCalculator
function getAssetInfo(address asset)
    external view returns (PegKind, AccrualKind, string[] memory labels);
```

It returns the enums rather than raw `uint8`s, so callers get a typed ABI rather than two integers to interpret.

{% hint style="info" %}
**`getAssetInfo` is declared on `NAVCalculator` itself, not on `INAVCalculator`.** Bindings generated from the interface alone will not carry it. Generate against the concrete contract, or add the one signature by hand.
{% endhint %}

**The narrower reads, the batch form and the probe live on the registry**, which the NAV names via `assetKindRegistry()`:

```solidity
// On AssetKindRegistry
function getAssetKind(address asset) external view returns (PegKind, AccrualKind);
function getAssetLabels(address asset) external view returns (string[] memory labels);

function getAssetKinds(address[] calldata assets)
    external view returns (PegKind[] memory pegKinds, AccrualKind[] memory accrualKinds);

function isClassified(address asset) external view returns (bool);
```

### What a caller actually receives

The two halves of `getAssetInfo` fail differently, and the difference is deliberate:

* **The classification half fails closed.** An unclassified asset **reverts** `AssetNotClassified` rather than reporting a default. The zero value of `AccrualKind` is `None` — a *real* classification held by 63 of the 93 configured assets — so a defaulting reply would be indistinguishable from a configured one. Probe with `isClassified` when an unclassified asset is an expected state.
* **The labels half never fails.** `getAssetLabels` does not revert; an unlabelled asset returns an **empty array**, which is a real answer rather than a gap.

That asymmetry is the whole rule: there is no defensible default classification, but "no labels" *is* the honest answer.

{% hint style="warning" %}
**A NAV that is not wired to a registry reverts rather than answering.** This is a different failure from an unclassified asset, and it looks nothing like an empty result: a consumer gets a revert, not a blank. Returning `(Pegged, None)` instead would be indistinguishable from a configured answer for a real stablecoin.

Read `assetKindRegistry()` to see which registry — if any — a given NAV points at. Wiring is a separate step from deploying either contract, so a NAV can be entirely correct while every classification read reverts.
{% endhint %}

***

## Display labels

Alongside the two axes, an asset carries an ordered list of **display labels** — free text for presentation, which nothing on-chain parses.

**Order is significant, and the list is not a set.** Index 0 is the protocol or issuer; later indices narrow to the product or kind:

```
["Aave V3", "Savings"]
["Lido", "LST"]
```

That broad-to-narrow **order** is the same one `protocolName()` and `positionLabels()` compose in — but the **content** differs, and the difference matters when you are writing the array. Asset labels carry the protocol at **index 0**. [Position labels](/funds/infrastructure/onchain-accounting/concepts/position-identity.md#human-readable-labels) never do: there, `protocolName()` supplies the protocol separately and `positionLabels()` starts at the product (`["Market", "wstETH/USDC"]`, `["Withdrawal Queue", "stETH"]`).

So the two are alike in ordering and opposite in what index 0 holds. Reading either array as an unordered set discards the only structure it has.

At most **4** labels, each at most **32 bytes**, none empty — `TooManyAssetLabels`, `AssetLabelTooLong` and `EmptyAssetLabel` reject the rest at write time. **Zero labels is legitimate**, not a misconfiguration.

**A wrong label can be corrected in place**, without touching the asset's registration or its price feeds:

```solidity
// On AssetKindRegistry — not on the NAV
function setAssetLabels(address asset, string[] calldata labels) external onlyNavManager;
```

It **replaces** the whole array rather than editing an element, and it can revert six ways. An integrator decoding errors needs to handle all six — the role gate, two of its own, and three content bounds it shares with `registerAsset`:

| revert                        | when                                                                                                |
| ----------------------------- | --------------------------------------------------------------------------------------------------- |
| `NotAuthorized`               | the caller does not hold the NAV's `MANAGER` role — checked first, so the likeliest one in practice |
| `AssetNotRegisteredForLabels` | the asset is not registered on the NAV                                                              |
| `EmptyAssetLabelSet`          | the array is empty                                                                                  |
| `TooManyAssetLabels`          | more than 4 labels                                                                                  |
| `AssetLabelTooLong`           | an element exceeds 32 bytes                                                                         |
| `EmptyAssetLabel`             | an element is the empty string                                                                      |

{% hint style="info" %}
**`setAssetLabels` replaces the whole list; it cannot empty one.** It rejects an empty array, so through this call the transitions are *none → some* and *some → other* only. Labels do still go from *some → none* — `unregisterAsset` clears them — but that ends the registration, which is not a label operation.

The **per-element** bounds are identical on both write paths — at most 4 labels, 32 bytes each, no empty element — so a label list that could not be registered cannot be set either. They differ in two places: the empty *array*, which `registerAsset` accepts (meaning "leave labels alone") and `setAssetLabels` rejects; and their preconditions, which are opposites — `registerAsset` requires the asset to be **absent** from the NAV, `setAssetLabels` requires it to be **present**. They are not interchangeable.

An asset can also be unlabelled because it never went through `registerAsset`: the chain's **native asset** is registered during initialization rather than by that call, so in practice `setAssetLabels` is how it gains labels.
{% endhint %}

{% hint style="danger" %}
**Never unregister an asset to fix its display strings.** `unregisterAsset` is not a label operation. It tears down the asset's entire pricing configuration — every price feed, every monitor feed, both tolerances — and takes the asset out of the NAV until it is registered again, so every consumer's NAV loses its balance in the meantime.

Re-registering does **not** restore that configuration. It sets one primary feed; everything else is rebuilt by hand, in an order that matters, and `pegToleranceBps` returning to 0 silently flips a stablecoin's [derived peg classification](#the-peg-axis-is-derived-not-stored) to `Floating`. Treat unregistration as removing the asset from the portfolio and re-onboarding it from scratch — see [Price feeds](/funds/infrastructure/onchain-accounting/price-feeds.md) for what that entails — not as an edit.

Correcting a wrong label requires none of it. That is what `setAssetLabels` is for.
{% endhint %}

***

## Adapter label overrides

[Adapter labels](/funds/infrastructure/onchain-accounting/concepts/position-identity.md#human-readable-labels) are produced by adapter code — `protocolName()` for the protocol, `positionLabels(positionId)` for the breadcrumb beneath it — so a wrong one is wrong for **every position that adapter serves**, not for one row. Redeploying the adapter is one way to fix that. It is no longer the only way.

Two per-adapter overrides are stored on the registry and applied at read time. All four calls are gated on the NAV's `MANAGER` role; the two **setters** additionally require the adapter to be registered, while the two **clearers** deliberately do not — so an override can still be removed after its adapter has been deregistered:

```solidity
// On AssetKindRegistry
function setAdapterProtocolName(address adapter, string calldata name) external;
function clearAdapterProtocolName(address adapter) external;

function setAdapterLabelCategory(address adapter, string calldata category) external;
function clearAdapterLabelCategory(address adapter) external;
```

An override that is unset falls through to whatever the adapter itself returns, so this changes presentation without touching the adapter. Setting one on an unregistered adapter reverts `AdapterNotRegistered`; an empty string reverts `EmptyAdapterOverride` — use the matching `clear…` call to remove one — and an over-long value reverts `AdapterOverrideTooLong`. **The clearers are not idempotent:** clearing an override that was never set reverts `AdapterOverrideNotSet`, so a cleanup batch run over several adapters fails on the first one that carried nothing.

**What the two overrides do not reach is any segment&#x20;*****beneath*****&#x20;index 0.** The category override replaces index 0 and nothing else; the computed leaf below it — which many adapters derive live from on-chain state, per position — is left untouched and still read live on every call. Correcting a leaf still means changing whatever produces it, the adapter code or its constructor argument, and redeploying that adapter. That is a **current limitation of the override surface**, stated as such: it is not a claim about what the surface was intended to be.

{% hint style="info" %}
**Both read paths agree.** The overrides apply on the **position** path — the verbose reads that render a position's breadcrumb — and on the **configuration** path, `NAVCalculator.getAdapterDisplayInfo(adapters)`, which returns each adapter's effective name and its positionId-independent breadcrumb. Pair that with `getAllAdapters()` to group configured adapters by exactly the string the position path will report.

**Read through the NAV Calculator, not the adapter.** A consumer that `staticcall`s `protocolName()` on an adapter directly reads the compiled-in value and never learns an override exists.

**Reading through the NAV is necessary but not sufficient.** The registry lookup is **fail-open**: if the registry is unwired, is an EOA, reverts, or answers with something unreadable, the read degrades silently to the adapter's own compiled-in name rather than failing. Replacing the registry drops every override the same way. So a correct consumer can still be served a stale name — the override surface is a presentation layer, not a guarantee.
{% endhint %}

{% hint style="warning" %}
**The override is keyed by&#x20;*****adapter*****, so it is only meaningful when index 0 is a literal&#x20;*****and*****&#x20;the same literal for every position that adapter emits.** Both halves are required, and a different adapter breaks each one.

For most adapters index 0 is exactly that — one fixed category, with the computed leaf at index 1 untouched and still read live. **One deployable adapter is excluded:**

* **`ERC20DefaultBalanceAdapter`** — index 0 is not a category at all. Its reply is a **single element**, which for an ERC-20 is a live `symbol()` read and for the native asset is the literal `"native"`. **An override replaces that element in either case** — the native leg is *not* exempt, because the replacement is applied whenever the array is non-empty, without inspecting what index 0 held.

**The exclusion is not enforced and not detectable.** The mechanism sees an array, not the code that produced it. It is an operational rule, asserted as a documented hazard in [`AdapterLabelOverrides.t.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/test/nav/AdapterLabelOverrides.t.sol).

{% hint style="success" %}
**`CapBalanceAdapter` used to be a second exclusion — it no longer exists.** Its index 0 was a literal that *varied by position* (`"Savings"` for stcUSD, `"Stablecoin"` for cUSD), so one per-adapter override rewrote both legs and correcting either silently corrupted the other.

It was **split** into [`CapStablecoinBalanceAdapter`](/funds/infrastructure/onchain-accounting/balance-adapters/cap-stablecoin.md) and [`CapSavingsBalanceAdapter`](/funds/infrastructure/onchain-accounting/balance-adapters/cap-savings.md) precisely to remove that shape: each now emits a single per-adapter constant at index 0, so overrides are **safe on both**. Cap was the only adapter in `src/balances/` in this state.

The per-position-literal mock survives in `AdapterLabelOverrides.t.sol` with no counterpart in `src/`, deliberately — the mechanism's limitation is unchanged and unenforceable, so the next adapter author reaching for a per-position literal needs the behaviour pinned rather than rediscovered.
{% endhint %}

**Before setting a category override, check the adapter against the notes on `setAdapterLabelCategory` in** [**`IAssetKindRegistry`**](https://github.com/karpatkey/onchain-accounting/blob/main/src/nav/IAssetKindRegistry.sol) — that is where the exclusions are maintained, and it is the record. Do not re-derive the rule from one adapter's labels: the condition is per-position stability, which a single position cannot show you.
{% endhint %}

{% hint style="info" %}
**What the override lookup costs on a verbose read.**

The figures are computed by [`test/nav/AdapterLabelOverrides.t.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/test/nav/AdapterLabelOverrides.t.sol), which measures this surcharge warm and cold and prints each number it derives. Run it for current values rather than budgeting from a figure quoted here — the harness is revised as the read paths are, and a number copied out of it is only true of the commit it was copied from. What that suite *enforces* is stated below, and each of these is an assertion: a change that breaks one breaks the build rather than silently ageing this page.

* **A miss is dearer than a hit, and every adapter is a miss today.** A hit skips the adapter's `protocolName()` staticcall; a miss pays the registry lookup **and still makes the adapter call**, so it is strictly the larger piece of work. No override has been set on any adapter, so the miss is what the system pays now. That saving is **specific to the protocol-name override** — it comes from skipping the adapter call. The category lookup is read on every verbose call whether or not one is set, so setting a category override does not make the read cheaper.
* **The surcharge scales with distinct adapters, never with positions.** Two adapters across three positions cost the same as two across two; the third position adds nothing. Budget by the number of distinct adapters a read touches, never by row count. Each bound defending this was set from a mutant that breaks it, not from taste.
* **Cold exceeds warm, and warm is a floor.** A warm measurement touches the slot before reading it; a real portfolio reads each adapter's slot cold the first time it touches it.

For an order-of-magnitude at operator scale, the cold account path over mainnet's registered adapter set costs roughly **9.5k gas per emitting adapter** — the shape a real portfolio has, rather than a single-adapter microbenchmark. Two caveats travel with that number and must not be separated from it: `vm.cool` restores cold **slots** but not the cold-**account** premium, so even the cold figures are a floor on what a fresh call pays; and the configuration surface's per-entry figure is measured with repeated addresses that are warm after the first touch, so it is a warm floor and must never be quoted as a per-distinct-adapter cost.
{% endhint %}

**The facade's `unregisterAsset` drops an asset's labels; `clearAssetKind` keeps them.** The two look interchangeable and are not:

* `clearAssetKind` ends the *classification* while the asset stays registered on the NAV — its labels still describe a live asset, so they are kept.
* `AssetKindRegistry.unregisterAsset` ends the *registration*, so it deletes the labels outright — after **this** call, and only this one, a later re-registration starts from none.

{% hint style="warning" %}
**That holds for the facade's `unregisterAsset`, not for the NAV's.** A `MANAGER` calling `NAVCalculator.unregisterAsset` directly — permitted by design, the same [two doors](#governance) as registration — **strands the labels**: the registry still holds them, and the facade's `unregisterAsset` can no longer clear them because it reverts inside the NAV, which no longer has the asset.

**The classification survives too**, and this is the part that misleads: `isClassified` still returns `true` and `getAssetInfo` still *answers* rather than reverting, so a consumer sees a live, classified, plausibly-labelled asset that the NAV holds nothing for. Unregistration also cleared the asset's tolerances, so if it *had* a peg tolerance the derived peg now reads `Floating` — but most assets never had one, so an unchanged `Floating` is not evidence that nothing happened.

**Getting out.** Re-register through the facade **with the correct labels**: given a non-empty array, `registerAsset` overwrites whatever is there, deliberately, since registration is what defines an asset's labels. One transaction. Re-registering with an *empty* array does not clear the stale set — an empty list means "leave labels alone" — and if that has already happened, `setAssetLabels` replaces them.

The two calls have **opposite preconditions**, which is easy to get backwards: `registerAsset` requires the asset to be **absent** from the NAV and reverts `InvalidArguments` if it is already registered, while `setAssetLabels` requires it to be **present** and reverts `AssetNotRegisteredForLabels` if it is not.

Re-registration fixes the labels, not the pricing. It restores one primary feed and leaves the tolerances and every monitor feed cleared — treat that as the [re-onboarding](#display-labels) it is.
{% endhint %}

***

## Governance

Registering an asset, classifying it and labelling it are **one operation**:

```solidity
// On AssetKindRegistry — the NAV has a registerAsset too, with a different signature
function registerAsset(
    address asset,
    address priceFeed,
    IPrices.PriceType priceType,
    uint256 chainlinkHeartbeat,
    AccrualKind accrualKind,
    string[] calldata labels
) external;
```

`AssetKindRegistry.registerAsset` stores the accrual kind and the labels, then forwards to the NAV's own `registerAsset` in the same transaction, so if either half reverts the whole transaction reverts and neither takes effect. An asset therefore cannot become registered-but-unclassified through a partly-applied change. Unregistration works the same way, and additionally drops the labels.

{% hint style="warning" %}
**That guarantee covers the facade, not the NAV.** `NAVCalculator.registerAsset` is still callable directly by any `MANAGER`, and it writes **neither** classification nor labels — so a registered asset having a classification is a property of *which door was used*, not of the system. This is the case where [`getAssetInfo` reverts `AssetNotClassified`](#what-a-caller-actually-receives) for an asset that really is registered and really is being priced.

**Both halves can now be repaired in place, in two transactions** — `setAssetLabels` for the labels, `setAssetKind` for the classification. Neither requires unregistering, and neither touches the asset's price feeds.

The two do not validate alike, though. `setAssetLabels` checks that the asset is registered on the NAV and reverts `AssetNotRegisteredForLabels` if not — so a mistyped address is caught. **`setAssetKind` has no such gate**: it rejects only `address(0)`, and will happily classify an address the NAV has never registered, emitting the event as though it were real. Nothing later reverts to reveal it.

What the facade still buys is **atomicity**, not exclusivity: `registerAsset` makes registration, classification and labels one transaction that cannot half-apply, whereas repairing after the fact leaves a window in which the asset is registered and priced but not yet classified — during which `getAssetInfo` reverts. Unregistering **through the facade** and re-registering remains the only way to *end* a label list, since `setAssetLabels` rejects an empty array — and it carries the [cost described above](#display-labels).

Whether the two doors are narrowed to one is a deployment-time choice about who holds `MANAGER`, not something the contracts settle. Treat "registered" and "classified" as separate questions and probe with `isClassified` when the answer matters.
{% endhint %}

Mutations — `registerAsset`, `unregisterAsset`, `setAssetKind`, `setAssetKinds`, `clearAssetKind`, `setAssetLabels` — are gated on the **NAV's `MANAGER` role**:

```solidity
// On AssetKindRegistry — the onlyNavManager modifier
if (!IAccessControl(NAV).hasRole(MANAGER, msg.sender)) revert NotAuthorized();
```

The registry holds **no roles of its own** and has no separate admin. Authority is exactly the NAV's `MANAGER` set — one set of humans for the whole system, rather than a second contract with its own governance to keep in step. The NAV address is set in the registry's constructor and immutable.

The pointer in the other direction is not immutable: a `MANAGER` aims the NAV at a registry with `setAssetKindRegistry`, which rejects `address(0)`. It is re-settable on purpose — the registry is a plain contract rather than a proxy, so replacing it means deploying a new one and re-pointing the NAV.

{% hint style="warning" %}
**"No roles of its own" is not "unprivileged" — both halves matter.** Because `registerAsset` forwards to the NAV, and the NAV gates that call on *its* caller, the registry itself must hold `MANAGER` on the NAV. So the registry **can register and unregister assets**, and a fault in it reaches the NAV's asset set. What the design removes is a *second role surface to keep in sync*; it does not remove privilege from the system.
{% endhint %}

The stored axis is written from per-asset configuration, where the accepted values are `None`, `Rebasing` and `Appreciating`, case-exact. A pre-deploy check rejects an absent, empty, mis-cased or unrecognised **value** — but it validates the value, **not the field set**: a misspelled *key* (`accrualkind`, `accrual_kind`) is silently ignored and reaches the deploy as if the field were absent.


---

# 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/concepts/asset-classification.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.
