# Guide: Writing a Custom Resolver

Resolvers are how a ChainRender app gets *any* on-chain data or media. ChainRender ships a large catalog, but you
can write your own — it's a tiny `view` contract.

## The interface

A resolver implements two functions:

```solidity
interface IChainRenderResolver {
    function supportsKind(bytes32 kind) external view returns (bool);
    function resolve(bytes32 kind, bytes calldata args) external view returns (string memory);
}
```

- `supportsKind` — a capability check (does this resolver serve this data kind?).
- `resolve` — read chain state and return a **string** (the value to display). The interpreter escapes it, so you
  can't inject markup even if you tried.

## Example: a token-balance resolver

```solidity
contract BalanceResolver is IChainRenderResolver {
    bytes32 public constant KIND = "tokenBalance";

    function supportsKind(bytes32 kind) external pure returns (bool) {
        return kind == KIND;
    }

    function resolve(bytes32 kind, bytes calldata args) external view returns (string memory) {
        if (kind != KIND) return "unavailable";           // fail closed on unknown kind
        (address token, address who) = abi.decode(args, (address, address));
        try IERC20(token).balanceOf(who) returns (uint256 bal) {
            return _format(bal);                            // your formatting
        } catch {
            return "unavailable";                           // fail closed on revert
        }
    }
}
```

## The rules that keep it safe (and Grade A)

- **`view` only.** A resolver reads; it never writes or moves value. This is why binding data keeps an app Grade A.
- **Fail closed.** Return an honest fallback ("unavailable") on any error — never revert the whole render, never
  fabricate a value.
- **No markup.** Return plain text; the interpreter escapes it. Never assume your output is trusted HTML.
- **Structured variants.** For charts/tables/routes, implement the richer interfaces (`IChartData`,
  `ITableSource`, `IRouteSource`) instead — same discipline, structured return.

## Binding it in your app

```ts
import { compose } from "@chainrender/client";
// a data slot pointing at your resolver + kind:
compose.slot("tokenBalance", { resolver: "0xYourResolver", arg: encodedArgs  }) // slot(bind, { resolver, arg });
```

## Testing your resolver's gas

A resolver bound to a slot runs under a gas stipend. Before binding a gas-heavy resolver to a value slot, probe
it: if it exceeds the stipend it should fail closed to the fallback (and you'd bind it as an image slot, which
gets a larger budget, instead). Test your resolver returns a sensible fallback under a tight gas limit.

## Related
- **[Resolvers & Data Binding](../concepts/resolvers.md)** · **[Reference → Resolver Catalog](../reference/resolvers.md)**
