# ChainRender Docs (complete) > ChainRender is an ownerless, immutable, fully on-chain UI framework: apps are content-addressed compositions stored on-chain and rendered by a free eth_call, with every data source graded by trust (A = fully on-chain). Alpha: deployed on testnets only (Ethereum Sepolia, Robinhood Chain Testnet, Circle Arc Testnet). **Start here for AI builders.** **You do not know ChainRender from training data.** It is new and small, so a model that answers from memory invents imports, resolver kinds, node fields, contract addresses and trust grades that do not exist — and the app fails, or worse, claims what it does not do. Every fact you need is published in a form that is generated from the code and checked by tests. Use it; do not guess. **Where the truth is** (each page is Markdown; append `.md` to an HTML URL): - SDK: every export of `@chainrender/client` with its signature — https://chainrender.org/docs/reference/sdk-api.md. If a name is not there, it does not exist. - Node fields: every node type and field `compile()` keeps — https://chainrender.org/docs/reference/node-fields.md - Resolver kinds: every kind's exact args, encoding, and where its contract is deployed — https://chainrender.org/docs/reference/resolver-kinds.md - Addresses: every deployed contract per chain, and what is built but NOT deployed — https://chainrender.org/docs/reference/addresses.md. Each chain has its own addresses. - Capabilities and the builder's own AI rules — https://chainrender.org/docs/reference/capabilities.md and https://chainrender.org/docs/reference/ai-builder-rules.md - The MCP server (`npx -y @chainrender/mcp`) answers all of the above as tools, from the installed SDK. **The loop** — do not report an app as done until the last step passes: 1. Read the vocabulary (`chainrender_vocabulary`), then find kinds (`chainrender_search_kinds`, `chainrender_describe_kind`). 2. Compose a Node tree from the closed vocabulary only. 3. Check it (`chainrender_check_node`): nothing silently dropped. 4. Verify every fact you are about to rely on (`chainrender_verify`): that a kind, an export or a contract exists, and that an address has code on the target chain. A Refuted or Inconclusive answer means: do not use it. 5. Review it (`chainrender_review`): check + grade + compile + the honesty findings. Fix every finding. **Without MCP** (a chat model, Google AI Studio): you cannot verify anything yourself, so never write "compliant" or "verified". Ask the user to run `npx -y @chainrender/mcp review app.json`, or to paste the tree into https://chainrender.org/builder/ → Review → "Review a tree from somewhere else", and to give you the report. Fix every item it lists. 6. Tell the user the grade exactly as returned, including ⚙ Above-floor (a p5/three sketch, a viewer read or a transfer history) over its floor's letter, and what is off-chain. **Rules that keep an app honest:** - Addresses come only from the Addresses page (or `chainrender_verify`) for the chain the app targets. Never invent one, and never reuse an address from another chain. A contract listed as "Built, not deployed" has no address. - Live data comes only from a slot or image bound to a resolver. Never hardcode numbers and call them live; a demo is titled as a simulation. - Grade A means every read is on-chain. A p5/three sketch, a viewer read and a transfer history are ⚙ (filled by the viewer above an on-chain floor), never "A · Trustless". An undeployed tree has no grade yet — say "would grade …". - The viewer's own wallet: `"$viewer"` as the account arg of `balance`, `balanceWei`, `erc20balance`, `erc20balanceRaw`, `ensName` (`who`), `identicon` or `gidenticon`, with no `resolver`; a transfer history is bind `transfers`. The viewer reads them only after the person opts in. Nothing else reads the connected wallet (no ERC-721 balance, no portfolio total), and `"$viewer"` never goes in an action, a signature or a condition. See https://chainrender.org/docs/guides/viewer-reads.md. - A `sign` message is signed exactly as written: inputs are never substituted into it, so no `$name` there. - A button's `$name` args are filled from the `input` with that `name` (`@chainrender/client` ≥ 0.13.0). Anything a user types into an action is written to the chain and is public, permanently — say so in the UI. - Two-player games: an on-chain lobby (`GameLobbyV2`), peer-to-peer signed moves, on-chain settlement (`GameChannelV2` + an `IGameRulesV2`). Start from `npm create chainrender@latest my-game -- --template game-tictactoe`. Real-time games (racing, physics) cannot be settled by rules: no stakes on them. - No ChainRender contract has had an external audit, and all are on testnets. Before an app holds value, read https://chainrender.org/docs/contracts/security-and-reviews.md and tell the user what it says about the contracts the app uses. Never call an app "audited", "certified", "official", "secure", "guaranteed", "insured", "bank-grade", "unhackable" or "risk-free": there is no certification program, and nothing in an app tests or insures anything. - Sketches run on the libraries deployed on-chain, never a CDN copy: **p5.js 2.3.2** (the 2.x API — one point per `bezierVertex` call, `splineVertex`/`splinePoint` instead of `curveVertex`/`curvePoint`, no `quadraticVertex`, and an `async` setup with `await loadImage(…)` because `preload()` is never called) and **three.js r160** (global `THREE`, no `import`). `chainrender_review` reports p5 1.x calls as an error. - A p5/three sketch is reviewed too: `chainrender_review` reads its source for unbacked claims and the CR-Stage mobile rules (touch-action, pinch-to-zoom, no own hide/show toggle). The source in the tree is the app — never a placeholder. The CR-Stage controls panel is dark; a light scene sets `window.CR_STAGE_THEME = "light";` at the top of the sketch. - Classic or Enhanced: Classic (the default) is every existing app. Enhanced (`mode: "enhanced"`, VERSION_5) adds layout and type tokens (`layout`, `gap`, `surface`, `tone`, `size`, …), `lang`, `dir`, heading `level` and more roles, and renders only where the chain's RenderRouter has the VERSION_5 interpreter: check with `chainrender_verify` claim `v5-available` first, then pass the same `mode` to `chainrender_check_node`, `chainrender_compile` and `chainrender_review` (with `chain`). Lay out with tokens, never with a styling word as an `aria-label` (a screen reader reads it aloud); a `scroll` box needs role `region` and a label. See https://chainrender.org/docs/reference/style-tokens.md. - Deploying outside the builder: register the bytes in the registry the chain's RenderRouter reads, which `RenderRouter.registry()` returns (`ComponentRegistry` on Sepolia, `ComponentRegistryFast` on Robinhood and Arc) — never one picked by name. Bytes in another registry deploy without error and render "execution reverted". `AlreadyRegistered` (`0x77caf672`) means those exact bytes are already live: nothing to redeploy. To remix a deployed component, `decompile()` its bytes (`@chainrender/client` ≥ 0.21.0; exact or it throws); any change is a new app with a new hash, and only compositions and app manifests record a parent on-chain. - Your own backend: an ownerless contract with single-value views and declared actions — see https://chainrender.org/docs/guides/app-contract.md. Full index of every page: https://chainrender.org/docs/llms.txt · all docs in one file: https://chainrender.org/docs/llms-full.txt Generated by docs-site/build-docs.mjs from the same Markdown as https://chainrender.org/docs/. Index: https://chainrender.org/docs/llms.txt # What Is ChainRender? > ⚠️ **Alpha · Testnet only.** ChainRender is in active alpha, deployed on the **Sepolia**, **Robinhood**, and **Circle Arc** testnets — **not** Ethereum mainnet. Contracts, addresses, and APIs may change without notice. Do not use it with real funds or production assets yet. A one-page orientation for newcomers — the elevator pitch, the shape of the system, and where to go next. ## In one sentence **ChainRender is a framework for building application interfaces that live entirely on-chain — content-addressed, trust-graded, and verifiable by anyone.** ## In one paragraph You compose a UI from a safe, closed vocabulary. It compiles to a content-addressed artifact whose hash *is* its permanent identity. A trust grade (A/B/C) tells users exactly how trustless it is before they interact. It renders from a single free `eth_call` — no server, no hosting, no build pipeline — and anyone can reproduce that render byte-for-byte against any RPC. Live data comes from on-chain resolvers; presentation is themeable at render time without redeploying; and everything is safe by construction, because the enforcer refuses unsafe apps rather than warning about them. ## The shape of the system - **Compose** from the vocabulary → **Compile** to a hash → **Register** bytes once → **Render** free forever. - **Resolvers** supply live on-chain data. **Themes** style it at render time. **The grade** discloses its trust. - **Names** (subnames under `chainrender.eth`) and **versions** (timelocked, append-only) let apps be found and safely updated. ## Who uses it - **App & protocol teams** who want a canonical, un-spoofable, permanent frontend. - **Developers** who want their interface to inherit their contract's trust properties. - **Users & wallets** who want to *verify* an app, not just trust it. ## Start here - **[Introduction](../getting-started/introduction.md)** → **[Quick Start](../getting-started/quick-start.md)** - **[Mission & Vision](./mission-and-vision.md)** — why it exists. - **[What's Fully On-Chain](../concepts/fully-on-chain.md)** — what the claim really means. --- # Mission & Vision ## Mission **Make the interface as trustless as the contract.** Blockchains gave us contracts nobody can secretly change — but we still reach them through websites somebody controls. ChainRender exists to close that last gap: to make the *user interface* — the thing people actually see, trust, and sign transactions in — as immutable, verifiable, and permanent as the smart contract behind it. No trusted frontend, no server that can lie, no interface that can vanish. ## Vision **A web where every app can be verified, not just trusted.** We envision a world where using an on-chain app doesn't require faith in whoever hosts its frontend — where you (or your wallet) can *prove* that the app you're using is exactly what it claims to be, that the button you're clicking does exactly what it says, and that nobody can swap it out from under you. A world where interfaces are public goods: permanent, forkable, composable, and owned by no one. ChainRender is infrastructure for that world. When the interface is on-chain, content-addressed, and honestly graded, "decentralized app" stops being half-true. ## What we're really building Not just a rendering library — a **trust model for user interfaces**. Everything in ChainRender follows from three commitments: 1. **The interface belongs on-chain.** If the contract is trustless but the frontend isn't, the app isn't. So the UI is stored and rendered on-chain, reproducible by anyone. 2. **Trust must be legible, never assumed.** An app's exact trust properties are computed and disclosed (the grade) — because *marketing must never exceed what the code enforces.* 3. **Trustless should also be rich and usable.** Being verifiable shouldn't mean being ugly or static. The floor principle lets apps be fully on-chain *and* genuinely rich; render-time theming lets developers iterate freely. ## Why it matters The frontend is where the trust actually happens. A user doesn't read bytecode — they read a button. If that button can be quietly changed, all the on-chain security in the world doesn't protect them. By moving the interface on-chain and grading its trust honestly, ChainRender protects users at the exact point where they're most vulnerable: the moment they decide to sign. ## Related - **[Principles & Ethos](../resources/ethos.md)** — the commitments, in engineering terms. - **[What's Fully On-Chain](../concepts/fully-on-chain.md)** — the mission, substantiated. - **[The Trust Grade](../concepts/trust-grade.md)** — trust made legible. --- # Brand & Identity ChainRender's brand is built the way ChainRender apps are built: **fully on-chain.** The logo isn't an image file on a server — it's generated on-chain, deterministically, and rendered from a `view` call. The brand *is* a demonstration of the product. ## The logo: an on-chain generative mark The ChainRender logo is an **on-chain gradient identicon** — a symmetric geometric mark computed deterministically from a brand identity, produced as SVG by an on-chain resolver at render time: ```ts import { grade, type Node } from "@chainrender/client"; import { getAddress, keccak256, toBytes } from "viem"; // The brand identity: an address derived deterministically from a label (what the repo helper // chainc/flagship/brand.ts brandAddress() computes — that helper is in the repo, not the npm package). const identity = getAddress(("0x" + keccak256(toBytes("chainrender")).slice(-40)) as `0x${string}`); // The logo is an image node the on-chain gradient-identicon resolver fills at render time — no image file, no CDN. const logo: Node = { type: "image", bind: "gidenticon", resolver: GRADIENT_IDENTICON_RESOLVER, // GradientIdenticonResolver on your chain (see Deployed Contracts) arg: identity, alt: "ChainRender logo", provenance: "computed", }; grade(logo).letter; // "A" ``` Properties that matter: - **Computed provenance (Grade A).** The logo is generated on-chain from a seed — there is no fetched image, no CDN. - **Deterministic.** The same identity always yields the same mark; it's reproducible by anyone. - **Data, not code.** A new brand mark is a new *seed*, not new code — additive and expandable by design. The generative **hero** art follows the same pattern: a deterministic Truchet-tile pattern, pure math (no script, no GPU), computed on-chain, Grade A. > This is dogfooding in the truest sense: ChainRender's own brand assets are on-chain ChainRender components. If we > wouldn't put our own logo on-chain, why would we ask you to put your app there? ## Two marks, one identity ChainRender uses **two complementary marks**, and the distinction is deliberate: 1. **The on-chain generative identicon (the product mark).** Grade A, computed on-chain, as above. This is the mark inside the flagship app and anywhere the point is to *show the thesis* — a logo that is itself a verifiable render. 2. **The interlocking-CR signature (the chrome mark).** A static SVG — a purple→pink **C** interlocking with a cyan→blue **R** — used for the *chrome* every project has: nav-bar logo, favicon, social avatars/headers, docs masthead. Chrome like a favicon is static on every product on earth; using a fixed signature there costs nothing on the floor and gives the site, builder, docs, and X account one recognizable face. The chrome mark **complements** the on-chain identicon; it does not replace it. Where a surface can afford a live `view`-call render (the flagship app, showcase heroes), the on-chain identicon stays the mark. ## Palette & type — the ChainRender theme The public brand is a first-class theme preset, `chainrenderDefaults()`, in `chainc/theme-tokens.ts` — a **fourth preset** alongside light / dark / high-contrast. Because the token set is the single source that derives *both* the `--cr-*` CSS variables (chrome + components) and the inlined palette image renderers use, one definition keeps the site, builder, docs, and on-chain renders uniform. It is **additive** (the existing themes are untouched) and, for now, **off-chain**; the matching on-chain `ThemeTokens` preset is a separate additive step. | Token | Value | Use | |---|---|---| | `bg` | `#0d1117` → `#241246` (135° gradient) | page background | | `fg` | `#e6edf3` | body text | | `mutedFg` | `#9a93ad` | secondary text | | `surface` / `panel` / `border` | `#191325` / `#120c1e` / `#2c2340` | cards, sidebars, rules | | `primary` | `#58a6ff` | links, primary actions | | `accent` | `#bc8cff` | emphasis | | brand gradient | `#7ee0ff → #bc8cff → #ff7b9c` | wordmark, headings, accents (`--cr-brand-gradient`) | | success / warning / info | `#3fb950` / `#d29922` / `#7ee0ff` | status | | chart series | `#58a6ff · #7ee0ff · #bc8cff · #ff7b9c · #3fb950 · #d29922` | data viz | **Type:** Inter for UI (falls back to `system-ui` where Inter isn't installed — image renders stay identical), `ui-monospace` for code and hashes. ## Favicon & app chrome The favicon/app icon is the interlocking-CR mark on a `#0d1117` rounded chip (`favicon.svg`), shared by the builder, docs, explorer, and site root so every tab reads the same. The wordmark "ChainRender" is set in the brand gradient (`#7ee0ff → #bc8cff → #ff7b9c`), left-to-right. ## The honesty label Every flagship ChainRender surface carries an **honesty label** — a small status line that states, plainly, the real conditions of what you're looking at (e.g. "Sepolia · synthetic liquidity (demo)"). It's a brand element *because* it's a principle: **marketing must never exceed what the code enforces.** The honesty label is that value made visible on the product itself. ## Voice & tone ChainRender's voice is **precise, honest, and unhyped**. We describe exactly what the code does — no more. We name boundaries plainly (e.g. "the rendered output is trustless; which RPC you read through is your choice"). We prefer "verifiable" to "trustless-marketing," and we'd rather under-promise and over-deliver than the reverse. If a claim can't be backed by a contract, it doesn't get made. ## Naming - **ChainRender** — the system. One word, capital C, capital R. - **`@chainrender/client`** — the SDK. **`create-chainrender`** — the scaffolder. - **`chainrender.eth`** — the root name; apps live at subnames (`myapp.chainrender.eth`). ## Using the brand Because the logo is on-chain and content-addressed, referencing it is a render, not a download — you compose the brand component and render it like any other. For static contexts, render the identicon SVG once and embed the output. The mark is a public good; use it to indicate ChainRender compatibility honestly. ## Related - **[Mission & Vision](./mission-and-vision.md)** · **[Principles & Ethos](../resources/ethos.md)** - **[Visual & Media Capabilities](../concepts/visual-media.md)** — the generative art the logo is built from. --- # Social & X Kit The ChainRender **X (Twitter) launch kit** — profile images, header banners, and launch copy — in three styles to pick from. It extends [Brand & Identity](./brand.md): same palette (the on-chain card `#0d1117` family), the gradient mark, and the honest, unhyped voice. Assets are authored as **SVG** (editable, on-ethos — "render once, embed the output") and rendered to **PNG** for upload. ## The three styles | Style | Look | Best for | |---|---|---| | **A — Terminal** | Dark `#0d1117`, monospace, gradient-identicon mark, Truchet tiles | Developer-native; matches the on-chain resolver cards | | **B — Gradient / Bold** | Vibrant gradient wordmark, **interlocking CR chain-link mark** (purple C cradling a blue R), capability pills | Highest scroll-stopping / viral punch | | **C — Blueprint / Minimal** | Blueprint grid, thin-line chain-link mark, typographic list | Premium, precise, understated | Pick one letter and use its matching header + profile + copy set (don't mix letters). ## Files (`brand/x/`) ``` brand/x/profile/{A-terminal,B-gradient,C-blueprint}-profile.svg (+ .png) 400×400 brand/x/header/ {A-terminal,B-gradient,C-blueprint}-header.svg (+ .png) 1500×500 brand/x/chainrender-mark.svg (+ .png) the interlocking-CR mark, transparent — favicon / logo reuse brand/x/X-posts.md launch post + thread, per style ``` Each **header lists what developers can build** with ChainRender (DeFi, RWA, reputation/EFP, cross-chain/CCIP, NFT & generative art, ENS, charts, 3D) — grounded in the real resolver capabilities. ## Logo & icon set (transparent, all sizes) — `brand/logo/` The interlocking-CR mark and wordmark lockups, on **transparent** backgrounds, for the site, app, favicon, and docs. ``` brand/logo/chainrender-mark.svg vector mark (transparent) — source of truth brand/logo/png/chainrender-mark-{16,32,48,64,128,180,192,256,512,1024}.png transparent mark, all sizes brand/logo/favicon.ico multi-size icon (16/32/48) for browsers brand/logo/chainrender-lockup-dark.svg (+ png 800/1600) mark + wordmark, WHITE text — for dark backgrounds brand/logo/chainrender-lockup-light.svg (+ png 800/1600) mark + wordmark, DARK text — for light backgrounds brand/logo/chainrender-lockup-stacked-dark.svg (+ png 520/1040) mark over wordmark (white) — square-ish contexts ``` Size guide: **16/32/48** favicon · **180** apple-touch-icon · **192/512** PWA/manifest · **256/512/1024** general. All mark PNGs are transparent; regenerate any size from `chainrender-mark.svg`. Use the **dark** lockup on dark backgrounds, the **light** lockup on light. The mark reads cleanly down to 16px. ## Specs & X placement - **Profile:** 400×400 (X displays it as a circle — the mark is centered and safe inside the circle). - **Header:** 1500×500. **Note X's overlaps:** on desktop the profile avatar sits over the lower-left of the header, and X crops the top/bottom on some viewports. Each header keeps its wordmark/mark and the capability list in the central band and away from the extreme bottom-left, but preview on a real profile before finalizing. ## Rerender PNGs from the SVGs The SVGs are the source of truth; regenerate PNGs any time (you already have `@resvg/resvg-js`/`sharp` from the share-card pipeline): ```bash # with sharp: node -e "const s=require('sharp'),f=require('fs');['profile/A-terminal-profile:400:400','header/A-terminal-header:1500:500'].forEach(x=>{const[p,w,h]=x.split(':');s(Buffer.from(f.readFileSync('brand/x/'+p+'.svg')),{density:200}).resize(+w,+h).png().toFile('brand/x/'+p+'.png')})" # or @resvg/resvg-js, or open the SVG in a browser and export. ``` For pixel-perfect brand type, install **Inter** (styles B/C) and a monospace (style A); otherwise the system fallback renders cleanly but not identically. ## Voice (from brand.md — keep it in replies too) Precise, honest, unhyped. Describe exactly what the code does. If asked "is it really trustless?", answer with the **A/B/C grade**, not a slogan. Marketing never exceeds what the code enforces. ## Handle Handle: **@chain_render** — profile at **https://x.com/chain_render**. Root name `chainrender.eth` is confirmed. Note: the `@chainrender/client` and `@chainrender/mcp` strings in the copy are **npm package scopes**, not the handle — leave them exactly as written. ## X account fields (ready to paste) Verified against X's limits: name ≤ 50, bio ≤ 160, location ≤ 30. **Name** (≤ 50) - `ChainRender` — clean, searchable *(11)* - `ChainRender · Fully On-Chain UIs` — name + one-line what *(33)* **Bio** (≤ 160) — pick one: | # | Bio | Chars | |---|-----|------| | 1 | Application UIs that live entirely on-chain. Composed, compiled to a hash, rendered from a view call. Verifiable · permanent · ownerless. Build before mainnet. | 160 | | 2 ⭐ | Your contract is immutable — now your frontend is too. Fully on-chain UIs, rendered from a view call & trust-graded A/B/C. No server, no hosting. | 147 | | 3 | The on-chain UI framework. compose → grade → compile → render. DeFi · RWA · reputation · identity · cross-chain. Verifiable, ownerless, render-free. | 158 | | 4 | Fully on-chain application UIs — rendered from a view call, honestly trust-graded, ownerless & permanent. The interface IS its hash. Build with the SDK or AI. | 160 | | 5 | On-chain frontends as verifiable as the contracts they front. Compose, grade, render — no server, no hosting. Live on Sepolia, Robinhood & Arc testnets. | 154 | ⭐ = recommended for launch (sharpest hook, literally true). Bio 5 is the alternative if you'd rather lead with the three-testnet proof. Bios 1 and 4 are exactly at the 160 ceiling — re-count if you tweak a word. **Website** — `https://chainrender.eth` (if the account accepts an ENS/eth.limo URL), else `https://chainrender.org`. **Location** (optional, ≤ 30) — `on-chain` *(8)* or `rendered from a view call` *(25)*. **Pinned:** pair the chosen bio with campaign post **01** as the pinned thread head (`brand/x/campaign/img/post-01.png`). ## Related - **[Brand & Identity](./brand.md)** — the on-chain generative logo, hero, and honesty label. - **[Mission & Vision](./mission-and-vision.md)** · **[Principles & Ethos](../resources/ethos.md)** --- # Introduction > **The one idea that matters:** a ChainRender app *is* a component compiled to bytes, registered on-chain, and > rendered by an `eth_call` — the component IS the app. Do **not** fetch data with viem and hand-render it in React > with ChainRender only printing a hash on the side; that is a conventional Web3 dashboard, not a ChainRender app. > See **[Building Correctly](../concepts/building-correctly.md)**. ## Start here — the fastest path to a correct app Do **not** hand-write a React app from scratch (it tends to drift into a viem dashboard that only prints a hash). Start from working code that already does the correct on-chain render loop: ```bash npm create chainrender@latest my-app ``` The scaffold emits a project whose render loop is correct by construction — it compiles a component and renders it on-chain via `renderComponent`, not by fetching data with viem and hand-rendering it. **Building with an AI agent?** Give it the **[MCP server & app-builder skill](../reference/mcp-and-skill.md)** first (`@chainrender/mcp` + the `chainrender-app-builder` SKILL.md, which includes a complete `examples/correct-react-app.tsx` reference). The skill teaches the compose → grade → compile → validate → render workflow and the anti-patterns, so the agent builds a real on-chain app instead of a conventional dashboard. This is the single most important step for AI-assisted builds. > ⚠️ **Alpha · Testnet only.** ChainRender is in active alpha, deployed on the **Sepolia**, **Robinhood**, and **Circle Arc** testnets — **not** Ethereum mainnet. Contracts, addresses, and APIs may change without notice. Do not use it with real funds or production assets yet. > **🎨 No install? Use the [visual builder](https://chainrender.org/builder)** — compose, grade, and deploy on-chain apps in the browser. > > **📦 Published on npm.** The ChainRender toolchain is live: > - **`@chainrender/client`** — the SDK · `npm install @chainrender/client` > - **`create-chainrender`** — project scaffolder · `npm create chainrender@latest my-app` > - **`@chainrender/mcp`** — the AI/MCP server · `npm install -g @chainrender/mcp` ChainRender is a framework for building application user interfaces that live **entirely on-chain** — no server, no hosting, no build pipeline, no trusted frontend. You compose a UI from a safe, closed vocabulary; it compiles to a content-addressed artifact whose hash *is* its permanent identity; and it renders from a single free `eth_call` that anyone can reproduce byte-for-byte against any RPC. ## The problem it solves Today, even a "decentralized" app usually has a centralized frontend: the smart contract is trustless, but the website that talks to it is hosted on a server someone controls. That server can change what you see, show you a different button than the one you think you're clicking, or disappear entirely. **The contract is immutable; the interface is not.** ChainRender closes that gap — the interface becomes as immutable, verifiable, and permanent as the contract it fronts. ## What makes it different - **Fully on-chain.** The UI is stored and rendered on-chain. There is no `index.html` on a server that could change under you. Rendering is a `view` call — free, and reproducible by anyone. - **Content-addressed.** An app *is* its hash. `renderHash(0xabc…)` always produces the same output, forever. A malicious viewer cannot swap "Send 1 ETH" for "Send 100 ETH" — the hash would change, and you'd know. - **Trust-graded.** Before you interact, a **grade** (A / B / C) tells you exactly how trustless the app is — whether it depends on any off-chain data, mutable pointers, or unproven assets — with an honest, itemized breakdown of every deviation. - **Safe by construction.** You cannot express an unsafe UI. The vocabulary is closed; actions are declared, not arbitrary calldata; capabilities are enforced on-chain. A component that would smuggle a hidden transaction is *refused at registration*, not caught later. - **Themeable without redeploying.** Colors, spacing, fonts, and chart dimensions are render-time parameters — a developer iterates on the whole page's look at **zero gas**, and only pays to commit a change when it's a real change to what the app *is*, not how it *looks*. ## Who it's for - **App developers** who want their interface to be as trustless as their contracts. - **Protocol teams** who want a canonical, un-censorable, un-spoofable frontend for a contract. - **Users** who want to *verify* that the app they're using is exactly what it claims to be. ## What you'll do next 1. **[Quick Start](./quick-start.md)** — render an existing on-chain component in five minutes. 2. **[Your First App](./first-app.md)** — compose, grade, and understand a component you built. 3. **[Concepts → Content Addressing](../concepts/content-addressing.md)** — the mental model that makes the rest click. > **A note on honesty.** ChainRender's core discipline is that *marketing never exceeds what the code enforces*. > Every claim in these docs corresponds to something the contracts actually guarantee. Where a guarantee has a > boundary (e.g. the *rendered output* is trustless, but *which RPC you read through* is a separate metadata > concern), the docs say so plainly rather than overclaiming. --- # Scaffolding a Project The fastest way to a working project is the scaffolder: ```bash npm create chainrender@latest my-app ``` You'll be asked **what you're building**: - **Single component (T1)** — a minimal HTML page that renders a deployed component by its hash from one `eth_call`. Best for embedding a component in an existing site. - **Mini-app (T2)** — a React project that composes an app and shows its tree check, content hash and grade live. - **Full app (T3)** — a DeFi app skeleton in the flagship's shape: two views with slots you bind to your chain's DeFi resolver. Best starting point for a real dApp frontend. - **Farcaster Mini App** — the render page plus a Mini App manifest stub. For CI or an AI agent, skip the prompts: `npm create chainrender@latest my-app -- --template miniapp-react`. Then: ```bash cd my-app npm install npm run dev # every template, HTML ones included (Vite serves the page so the SDK import resolves) ``` Every scaffold **starts at grade A**, and the React templates show the live `grade()` and `checkNode()` result as you build. To render a deployed component, set the chain's RenderRouter address and your app hash (the HTML page asks for them; React reads them from `.env` — copy `.env.example`). The scaffolder is manifest-driven: new templates are added as manifest entries, never as forked code. **Next:** **[Your First App](./first-app.md)** to understand what the scaffold generated. --- # Quick Start > **The one idea that matters:** a ChainRender app *is* a component compiled to bytes, registered on-chain, and > rendered by an `eth_call` — the component IS the app. Do **not** fetch data with viem and hand-render it in React > with ChainRender only printing a hash on the side; that is a conventional Web3 dashboard, not a ChainRender app. > See **[Building Correctly](../concepts/building-correctly.md)**. Render a fully on-chain component in five minutes. No build pipeline, no server — just an RPC and the SDK. ## Install ```bash npm install @chainrender/client viem ``` ## Render an existing on-chain app Every ChainRender app is addressable by a hash. Rendering it is a single free `eth_call`: ```ts import { renderComponent } from "@chainrender/client/runtime"; const html = await renderComponent({ rpc: "https://ethereum-sepolia-rpc.publicnode.com", router }, "0x…", // a deployed app component hash ); document.getElementById("app").innerHTML = html; // fully on-chain output ``` That's it. The HTML you just rendered came entirely from the chain. Reload it against a *different* RPC and you'll get byte-identical output. That reproducibility is the point: **anyone can verify the app is what it claims to be.** ## Compose and grade your own Before you deploy anything, compose an app from the vocabulary and grade it locally — the grade you see equals what the chain will compute (they're drift-locked): ```ts import { compose, compileComponent, grade } from "@chainrender/client"; const app = compose.box([ compose.heading("Claim your allocation"), compose.text("A fully on-chain claim page."), compose.button("Claim", { variant: "primary", action: { to: "0x…", sig: "claim(uint256)", args: "$amount" }, }), ]); const built = compileComponent(app); // → content-addressed artifact const g = grade(app); // → { letter: "A", word: "Trustless", … } console.log(built.hash); // this hash IS the app's identity, forever console.log(g.letter, g.word); // the trust grade, before you deploy ``` ## Scaffold a whole project To start from a working template instead of a snippet: ```bash npm create chainrender@latest my-app ``` You'll be asked what you're building (a single component, a mini-app, or a full app) and get a wired, rendering, **grade-A** project — see **[Scaffolding a Project](./scaffolding.md)**. ## Next steps - **[Your First App](./first-app.md)** — build and understand a component end-to-end. - **[Concepts → The Render Pipeline](../concepts/render-pipeline.md)** — what actually happens when you render. - **[Guides → Bind Live On-Chain Data](../guides/bind-data.md)** — make your UI show real, live chain state. --- # Your First App > **The one idea that matters:** a ChainRender app *is* a component compiled to bytes, registered on-chain, and > rendered by an `eth_call` — the component IS the app. Do **not** fetch data with viem and hand-render it in React > with ChainRender only printing a hash on the side; that is a conventional Web3 dashboard, not a ChainRender app. > See **[Building Correctly](../concepts/building-correctly.md)**. Build, grade, and understand a real component end-to-end. ## 1. Compose ```ts import { compose } from "@chainrender/client"; const app = compose.box([ compose.heading("My First On-Chain App"), compose.text("This UI lives entirely on-chain."), compose.button("Do it", { variant: "primary", action: { to: "0x…", sig: "act(uint256)", args: "$n" } }), ]); ``` You composed from the **vocabulary** — a closed set of primitives. You literally cannot express anything outside it, which is what makes the next step meaningful. ## 2. Compile — get your app's identity ```ts import { compileComponent } from "@chainrender/client"; const built = compileComponent(app); console.log(built.hash); // 0x… — this IS your app, forever ``` The hash is content-addressed: it's the `keccak256` of your app's exact bytes. Change anything and the hash changes. See **[Content Addressing](../concepts/content-addressing.md)**. ## 3. Grade — know your trust level before deploying ```ts import { grade } from "@chainrender/client"; const g = grade(app); console.log(g.letter, g.word); // "A" "Trustless" ``` Your app is grade A because it's fully on-chain with no external dependencies. If you'd added an off-chain image, you'd see it drop to C — with an itemized reason. This grade equals what the chain computes (drift-locked). See **[The Trust Grade](../concepts/trust-grade.md)**. ## 4. Register — pay gas once Register the bytes in the registry the chain's `RenderRouter` reads — `RenderRouter.registry()` returns it: `ComponentRegistry` on Sepolia, `ComponentRegistryFast` on Robinhood and Arc (one transaction). After that, anyone renders your app for free, forever, from any RPC. ## 5. Render — free, reproducible ```ts import { renderComponent } from "@chainrender/client/runtime"; const html = await renderComponent({ rpc, router }, built.hash ); ``` ## What you learned - Composition is constrained (safe by construction). - The hash is the identity (immutable, verifiable). - The grade discloses trust (honest, itemized). - Rendering is a free, reproducible `eth_call`. **Next:** **[Bind Live On-Chain Data](../guides/bind-data.md)** to make it show real chain state. --- # Guided Tours Short, **complete, copy-paste** walkthroughs that take you from an empty canvas to a live, graded, fully on-chain app — with the real gotchas called out at the exact step they bite. Every other section explains *concepts* and *single tasks*. A Guided Tour is different: it is one unbroken path through the **whole build process** for a real app, with the exact JSON, the exact commands, the exact errors you might see, and why. If you have ever gotten a cryptic builder error and not known whether it was your app, your config, or the chain — these tours are the antidote. ## How these tours work Each tour follows the same spine, so once you've done one the rest are fast: 1. **Build or import** the app (we give you the full JSON to import, so you see the UI before writing anything). 2. **Preview** it in the Visual Builder and read the **grade badge**. 3. **Fix the gotchas** — the import/compile pitfalls, explained where they happen. 4. **Point it at the chain** — set the Interpreter + RPC, then deploy and seed the backing contracts. 5. **Go live** — confirm real data renders and the grade holds. Everything here is honest about grade: a value only claims **Grade A** when it is an on-chain read. Where a capability has an off-chain half (an event-log feed, a client-side artifact hash), the tour says so and shows the ≤ C label — it never dresses one up as the other. See **[The Trust Grade](../concepts/trust-grade.md)**. ## Before you start - **Open the Visual Builder** — `/builder/index.html`. Import lives under **⋯ More → Import JSON**. - **A browser wallet** (for the deploy/seed steps only; import + preview need no wallet). - **An RPC** for your target testnet — Ethereum **Sepolia** (11155111), **Robinhood Chain** (46630), or **Circle Arc** (5042002). Set it in the builder's config bar (the gear). - **Up-to-date builder vocabulary.** The builder renders from an embedded snapshot of the resolver vocabulary. If a tour's resolver kind doesn't appear in the slot picker, your snapshot is stale — regenerate it from the source of truth (one command, shown in each tour): ```bash npx tsx scripts/sync-builder-vocab.mjs # VOCAB.resolverKinds <- chainc; CAPABILITY_PACK, AI_RULES <- ai/*.json ``` ## The tours | Tour | You'll build | New capabilities exercised | |---|---|---| | **[Your First Grade-A App: Settlement Desk](./settlement-desk.md)** | a settlement agreement card | `settle-status`, `settle-principal`, `settle-repaid`, `trust-facets` | | **[Proof & Compute](./proof-and-compute.md)** | a trusted-setup pin + metered-credit card | `proof-setup`, `compute-credit`, `trust-facets` | | **[Doorstep](./doorstep.md)** | a public on-chain tribute | `door-knocks`, the identicon floor, `trust-facets` (and the honest ≤ C `door-feed`) | Start with **[Settlement Desk](./settlement-desk.md)** — it carries the full explanation of the import/compile pitfalls that every builder user hits once. The other two reuse that spine and focus on what's new. > These three apps are also provided as ready-to-import files in `builder/showcase/` in the > repo. The JSON in each tour is the same, inline, so the tour is self-contained. --- # Tour: Your First Grade-A App — Settlement Desk You'll build a **Settlement Desk**: a card that reads a settlement agreement's live on-chain state (status, pooled principal, percent repaid) and shows its self-declared trust facets. Every value is an on-chain read, so the whole app is **Grade A**. Along the way you'll hit — and fix — the one import/compile pitfall every builder user meets once. Time: ~10 minutes to a rendering preview; add the deploy/seed steps to see live data. --- ## 1. Import the app Open the Visual Builder (`/builder/index.html`) → **⋯ More → Import JSON** → paste this file. It's a bare node tree — exactly what Import accepts and what **Export JSON** produces. ```json { "type": "box", "direction": "column", "children": [ { "type": "text", "content": "Settlement Desk", "role": "heading" }, { "type": "text", "content": "A fully on-chain settlement agreement, rendered straight from chain state." }, { "type": "text", "content": "Live settlement state", "role": "heading" }, { "type": "box", "direction": "row", "children": [ { "type": "slot", "bind": "settle-status", "argValues": { "agreement": "0x1111111111111111111111111111111111111111", "assetDecimals": "6", "fracDigits": "2" } }, { "type": "slot", "bind": "settle-principal", "argValues": { "agreement": "0x1111111111111111111111111111111111111111", "assetDecimals": "6", "fracDigits": "2" } }, { "type": "slot", "bind": "settle-repaid", "argValues": { "agreement": "0x1111111111111111111111111111111111111111", "assetDecimals": "6", "fracDigits": "2" } } ] }, { "type": "text", "content": "Trust, declared on-chain", "role": "heading" }, { "type": "slot", "bind": "trust-facets", "argValues": { "subject": "0x1111111111111111111111111111111111111111" } } ] } ``` The canvas renders the layout immediately. The `0x1111…1111` addresses are **placeholders** — you'll replace them with your deployed agreement in step 5. Until then, the slots read nothing, which is exactly what step 4 is about. --- ## 2. A heading is a `text` node with a role — not `type: "heading"` **This is the gotcha.** It is tempting to write a heading as `{ "type": "heading", … }`, and the canvas will even *draw* it — so it looks fine. But when you **compile** it with the SDK's `compile()` (or **preview on-chain** in a builder from before its heading safety net), you get: ``` On-chain preview failed: u8 out of range: undefined. ``` That error is **not** about your RPC or interpreter, even though the builder suggests checking them. It is thrown **client-side, during compile, before any `eth_call`**. The compiler turns each node's `type` into a one-byte tag, and the set of real node types is: > `text · box · button · input · icon · link · slot · image · ref` `heading` is **not** one of them. The canvas renderer tolerates `type: "heading"` for display, but the compiler doesn't recognize it, so the tag comes back `undefined` and the byte writer throws `u8 out of range: undefined`. The renderer and the compiler disagreed, and the compiler is the one that matters for anything that touches the chain. **The fix** (already applied in the JSON above): a heading is a `text` node carrying the ARIA heading role. ```json { "type": "heading", "content": "Settlement Desk" } // ✗ compiles to u8 out of range { "type": "text", "content": "Settlement Desk", "role": "heading" } // ✓ a real, compilable heading ``` Rule of thumb: **if a value isn't one of the nine node types, it belongs in an attribute** (`role`, `variant`, `provenance`, `direction`), not in `type`. See **[Gotchas, Do's & Don'ts](../resources/gotchas.md)**. > **Now caught for you.** The current builder rewrites a `type: "heading"` node into this form before every compile > (`encodeBindings`), so the builder no longer throws. Outside the builder — the SDK's `compile()`, or JSON an AI wrote — > it still does. Run `checkNode(tree, { authoring: true })` (SDK 0.10.1+, or the MCP `chainrender_check_node` tool) > first: it reports a bad `type`, along with every other field `compile()` would silently drop, such as a `role` that > isn't in the dictionary. Every allowed field and value is on **[Node Fields](../reference/node-fields.md)**. --- ## 3. Make sure the builder knows the resolver kinds `settle-*` and `trust-facets` must appear in the slot's **resolver kind** picker. If they don't, your builder's embedded vocabulary is an older snapshot. Regenerate it from the one source of truth and rebuild the site: ```bash cd /h/chainrender_deploy/chainrender-project npx tsx scripts/sync-builder-vocab.mjs # VOCAB.resolverKinds <- chainc; CAPABILITY_PACK, AI_RULES <- ai/*.json (edits only those) # then your normal site build regenerates builder/index.html from builder/index-next.html ``` Confirm the kinds are in the source of truth first: ```bash npx tsx -e "import('./chainc/resolverkinds.js').then(m=>console.log(m.RESOLVER_KINDS.map(k=>k.kind).join(' ')))" \ | tr ' ' '\n' | grep -E 'settle-|trust-facets' ``` --- ## 4. Preview, and read the fail-closed state Hit **Compile / Preview**. With the fix from step 2, compile succeeds and the builder calls the interpreter's `render(bytes)` via `eth_call`, using the **Interpreter address + RPC** in the config bar (the gear). Two honest outcomes: - **Interpreter/RPC not set** → now you get a *real* interpreter/RPC error (this time the builder's hint is right). Set a valid interpreter for your chain and an RPC URL, then retry. - **Set, but placeholder addresses** → the layout, headings, and the **grade badge** render, and each slot shows **"unavailable."** That is the design: a Grade-A read **fails closed** rather than inventing a value. Nothing is wrong — you simply haven't pointed it at a real agreement yet. > Why "unavailable" and not an error? `settle-status` is a view read. With no real agreement at > the address, there's nothing to read, so the honest render is "unavailable." See > **[The Floor](../concepts/the-floor.md)**. --- ## 5. Point it at a real agreement (deploy + seed) To see live numbers, deploy the stateless resolvers once per chain and a real agreement to read. These contracts are **ownerless and immutable** — additive, no migration. Never put your deployer key in shell history: ```bash read -rs DEPLOYER_PK ; export DEPLOYER_PK # hidden input; or: npx hardhat vars set DEPLOYER_PK echo # deploy SettlementCardResolver + TrustDisclosureResolver to your target chain (see the deploy guide # for the exact script/flags for your setup), then deploy/seed an AttestedLoanChannel (or any # ISettlementView) so there is real state to read. ``` Then, in the builder, replace **`0x1111…1111`** in all four slots with your deployed agreement address, set `assetDecimals` to the escrow asset (6 = USDC, 18 = native), and preview again. The status word, principal, and repaid-percent now render from chain, and `trust-facets` shows the agreement's declared custody / exit / relayer-binding. See **[Deploying an App](../guides/deploying.md)** and **[Reading On-Chain](../contracts/reading.md)** for the per-chain specifics. --- ## 6. Confirm the grade Check the grade badge: it should read **Grade A** — every slot is a `READ_CHAIN` resolver with an on-chain data edge. If you add an off-chain bind later (an event feed, a client fetch), the badge drops to ≤ C **by construction** and the honest label appears. That guard is the point: the badge can never over-claim. You've now done the full loop — import → fix → preview → deploy → live → grade. The next two tours reuse this spine. **Next:** **[Proof & Compute](./proof-and-compute.md)** → --- # Tour: Proof & Compute Two zipcoin-derived capabilities, both **Grade A** on-chain reads: a trusted-setup artifact **pinned by SHA-256** (content-addressed, append-only) and a play-key's **metered compute credit** drawn down against EIP-712-signed receipts. This tour assumes you've done **[Settlement Desk](./settlement-desk.md)** — the import, the heading rule, the vocab-sync, and the fail-closed preview all work the same way here, so we focus on what's new. --- ## 1. Import **⋯ More → Import JSON** → this tree. (Note: headings are `text` + `role:"heading"` — see [the heading gotcha in Tour 1](./settlement-desk.md), step 2.) ```json { "type": "box", "direction": "column", "children": [ { "type": "text", "content": "Proof & Compute", "role": "heading" }, { "type": "text", "content": "Trusted-setup pin", "role": "heading" }, { "type": "slot", "bind": "proof-setup", "argValues": { "registry": "0x4444444444444444444444444444444444444444", "id": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, { "type": "text", "content": "Compute credit (play-key)", "role": "heading" }, { "type": "slot", "bind": "compute-credit", "argValues": { "vault": "0x5555555555555555555555555555555555555555", "playKey": "0x6666666666666666666666666666666666666666", "assetDecimals": "6", "fracDigits": "2" } }, { "type": "text", "content": "Trust, declared on-chain", "role": "heading" }, { "type": "slot", "bind": "trust-facets", "argValues": { "subject": "0x5555555555555555555555555555555555555555" } } ] } ``` --- ## 2. The trusted-setup pin — what's Grade A, and what isn't `proof-setup` reads a `SetupPinRegistry`: the pin's existence, its pinner, and its URI are on-chain truth — **Grade A for the read.** What it does **not** prove is that the pinned digest is the *real* setup; that rests on the pinner. The teeth are the **≤ C** companion `artifact-verify`: the client fetches the artifact, SHA-256s it, and compares to the pinned digest. ChainRender renders the on-chain pin honestly and names the pinner; it never says "trusted," only "matches the on-chain pin" (or not). - `registry` — your deployed `SetupPinRegistry`. - `id` — the content-addressed pin id, `keccak256(abi.encode(sha256, uri))`. To add the ≤ C verification next to it, add an `artifact-verify` slot with `{ expectedSha256, uri }`. Its grade badge will read ≤ C by construction — that's the honest split, not a downgrade of your app. --- ## 3. Compute credit — escrow is on-chain, the compute is not `compute-credit` reads a `ComputeCreditVault`: **funded / spent / remaining** for a play-key — **Grade A.** The provider can only draw down against the play-key's **EIP-712-signed usage receipts**, so it can never overcharge, and the play-key withdraws any unspent balance at any time. The **compute being paid for is off-chain** (attested, ≤ C) — the card says so. ChainRender renders the *credit*, never the compute. - `vault` — your deployed `ComputeCreditVault`. - `playKey` — the scoped play-key address whose credit to show. When you wire the real draw-down, sign receipts with the SDK's `usageReceiptTypedData(...)` helper so the domain + type match the vault's `DOMAIN_SEPARATOR` and `USAGE_RECEIPT_TYPEHASH` exactly. --- ## 4. Preview, deploy, grade Same as Tour 1: with placeholder addresses the slots render **"unavailable"** (fail-closed); set the Interpreter + RPC in the config bar; then deploy `ProofBadgeResolver`, `ComputeCreditResolver`, and `TrustDisclosureResolver` (ownerless, once per chain) and seed a `SetupPinRegistry` with a pin and a `ComputeCreditVault` with a funded play-key. Replace the `0x4444…`, `0x5555…`, `0x6666…` placeholders and preview again — the pin and the credit render live, **Grade A**. **Next:** **[Doorstep](./doorstep.md)** → --- # Tour: Doorstep A **public on-chain tribute**. A knock burns a token to a door; the aggregate — total burned, knock count, loudest knock — is read from a `DoorRegistry` view (**Grade A**). The message history lives only in event logs and is deliberately *not* rendered as Grade A — ChainRender renders the public tribute, never an inbox. This tour reuses the spine from **[Settlement Desk](./settlement-desk.md)**. --- ## 1. Import ```json { "type": "box", "direction": "column", "children": [ { "type": "text", "content": "Doorstep", "role": "heading" }, { "type": "text", "content": "A public on-chain tribute. The identicon is the pure on-chain floor; the aggregate is a Grade-A view read." }, { "type": "box", "direction": "row", "children": [ { "type": "image", "bind": "gidenticon", "provenance": "computed", "argValues": {} }, { "type": "slot", "bind": "door-knocks", "argValues": { "registry": "0x2222222222222222222222222222222222222222", "target": "0x0000000000000000000000003333333333333333333333333333333333333333", "assetDecimals": "18", "fracDigits": "4" } } ] }, { "type": "text", "content": "Trust, declared on-chain", "role": "heading" }, { "type": "slot", "bind": "trust-facets", "argValues": { "subject": "0x2222222222222222222222222222222222222222" } } ] } ``` --- ## 2. The identicon is the floor — it needs nothing deployed The `image` node bound to `gidenticon` is the **pure on-chain-deterministic floor**: a symmetric identicon computed from an address with keccak, in the browser. Same address → same SVG, forever, no contract and no network. It renders on import, before you touch a chain. See **[The Floor](../concepts/the-floor.md)** and **[On-Chain Icons](../concepts/icons.md)**. ## 3. `door-knocks` is Grade A — the message feed is not `door-knocks` reads the `DoorRegistry` aggregate via a view: **Grade A.** - `registry` — your deployed `DoorRegistry`. - `target` — the door key: `bytes32(uint160(address))` for an address door, or an ENS namehash. (The example pads an address into 32 bytes.) The **messages** are a separate, honest capability: `door-feed` reads the `Knock` **event logs** via `eth_getLogs`. That's on-chain *data* but not a re-executable view, so it grades **≤ C** and is labeled off-chain — never dressed as Grade A. If you want the message list, add a `door-feed` slot with `{ registry, target }`; its badge will correctly read ≤ C. Keeping the aggregate (Grade A) and the feed (≤ C) as two binds is the point: the grade on each is honest. ## 4. Preview, deploy, grade As before: placeholders → `door-knocks` shows **"unavailable"** (the identicon still renders); set Interpreter + RPC; deploy `DoorResolver` + `TrustDisclosureResolver` (ownerless, once per chain) and a `DoorRegistry` that's had at least one `knock`, so there's an aggregate to read. Replace `0x2222…` and the `target`, preview, and the tribute renders live, **Grade A**. That's the full set. You've imported, fixed the compile gotcha, previewed fail-closed, synced the vocabulary, and taken an app to live on-chain data — the complete ChainRender build loop. ← **[Back to Guided Tours](./index.md)** --- # Overview & Mental Model ChainRender has five moving parts. Once you can name them, the whole system fits in your head. 1. **The Vocabulary** — a closed set of UI primitives (box, text, button, image, chart, table, controls, …) you compose an app from. You cannot express anything outside it, which is what makes apps analyzable and safe. 2. **The Compiler** — turns your composition into content-addressed bytes. The hash of those bytes *is* the app. 3. **Resolvers** — on-chain contracts that supply *live data* (a price, a balance, an ENS name, a chart series). Your UI binds to them; they're read at render time. 4. **The Enforcer & Grade** — proves what an app can do, refuses unsafe ones, and assigns an honest trust grade. 5. **The Renderer** — a `view` function that turns a hash into HTML/SVG, deterministically, for free. The flow: **compose → compile (get a hash) → register bytes once → render for free forever**, with resolvers supplying live data and the grade disclosing trust. Presentation (theme) is a render-time parameter layered on top; versioning is an explicit registry layered on top. Everything else is immutable. ## Start here — build it correctly Before the feature list: a ChainRender app is a **compiled component, registered on-chain, rendered by an `eth_call`** — the component *is* the app. The most common mistake is building a normal React app that fetches data with viem and only uses ChainRender to display a hash. That is not a ChainRender app. See **[Building Correctly](./building-correctly.md)** for the sharp line + the compile→register→render flow. **Building with an AI?** Give it the **[MCP server & app-builder skill](../reference/mcp-and-skill.md)** so it builds correctly by default. ## Capability map — what you can build ChainRender is one primitive (compose → compile → register → render) applied across many capability areas. Each links to its guide: - **UI & components** — the [render pipeline](./render-pipeline.md), the closed [vocabulary](./resolvers.md), and [theming](./theming.md) applied at render time. - **Live on-chain data** — [resolvers](./resolvers.md) read prices, balances, ENS, attestations, and any contract's view state, [rendered honestly](./trust-grade.md). - **DeFi** — a complete [DeFi capability suite](./defi-capabilities.md): market prices, a user's LP + lending positions (with health factor), pre-trade quotes, approval-state gating, Curve, portfolio totals, and [Uniswap hook rendering](./rendering-uniswap-hooks.md). - **RWA (real-world assets)** — a [RWA capability suite](./rwa-capabilities.md): Proof-of-Reserve coverage, ERC-3643 (T-REX) compliance display, and issuer-bound credential lifecycle — display compliance, never *become* the compliance authority. - **Reputation & social** — a [reputation capability suite](./reputation-capabilities.md): the EFP social graph (forward follow/tag/block/mute, Grade A), delegated address vouching (`rep-vouch`/`rep-tally`), a cross-source aggregate (`rep-aggregate`, grade capped at the weakest source), and ERC-725Y / ERC-735 account claim reads (the standard behind LUKSO Universal Profiles — read on the chain the resolver is deployed on, ≤ C; LUKSO's own chain is not integrated) — read + render reputation, never mint a score. - **Actions & payments** — declared [actions](./actions.md) decoded before signing, and [payments](./payments.md) (crypto floor + optional off-chain card on-ramp, honestly graded). - **Rich media** — [p5.js & three.js](./rich-media-p5-three.md) running over an on-chain floor, plus [on-chain icons](./icons.md) and [advanced scenes](./advanced-scenes.md). - **Composition & multi-view** — [composing apps](./composition.md) from registered components. - **Discovery & sharing** — [Explore](./apps-directory.md) (catalog components, plus deployed scenes and submitted apps read live from the chain) and short share links (`/app//`). - **Trust & privacy** — the [trust grade](./trust-grade.md), the [enforcer](./enforcer.md), [trust signals](./trust-signals.md), and [privacy](./privacy.md). - **Deploying & scaling** — [L2 deployment strategy](./l2-deployment-strategy.md), [size limits & Glamsterdam](./size-limits-and-glamsterdam.md), and [testing](./testing.md). - **Where Ethereum is heading** — how upcoming protocol changes meet the render-and-declare, read-first, ownerless model: bigger contracts but pricier storage ([Size Limits & Glamsterdam](./size-limits-and-glamsterdam.md)) and balances proven across chains instead of bridged ([Proven Balances](./cross-chain-balances.md)). See **[The Render Pipeline](./render-pipeline.md)** for the step-by-step, and **[Content Addressing](./content-addressing.md)** for the foundation. --- # Building Correctly — Your App IS the On-Chain Component The single most common mistake when building on ChainRender is treating it as *a library a React app calls to produce a hash*, instead of *the framework the app is built in and rendered from*. This page draws that line sharply, because getting it wrong produces an app that looks like ChainRender but isn't on-chain at all. > **Building with an AI agent?** Point it at the **[ChainRender MCP server & app-builder skill](../reference/mcp-and-skill.md)** > (`@chainrender/mcp` + the `chainrender-app-builder` SKILL.md). The skill teaches the correct > compose → grade → compile → validate → render workflow and the anti-patterns below, so the agent builds a real > on-chain app instead of a conventional dashboard. This is the recommended path for AI-assisted builds. ## A complete, copyable example This is the whole correct loop in one place. The balance is read **and rendered on-chain** by the `Slot` — you never fetch it yourself. Clone this shape (a full runnable version ships in the app-builder skill as `examples/correct-react-app.tsx`): ```tsx import { compileJsx } from "@chainrender/client"; import { renderComponent } from "@chainrender/client/runtime"; const DEPLOYMENT = { rpc: "", router: "", registry: "" }; // 1. COMPILE — the Slot declares an on-chain read; the interpreter resolves it at render time. const { hex, hash } = compileJsx(` Account Balance `); // 2. REGISTER — write the bytes on-chain once (wallet tx to the registry RenderRouter.registry() returns, with `hex`). // After this, the component lives on-chain forever, addressed by `hash`. // 3. RENDER — one eth_call returns the fully on-chain-rendered HTML, balance included. Display THAT. const html = await renderComponent(DEPLOYMENT, hash); document.getElementById("app")!.innerHTML = html; // ← the on-chain UI. Not hand-built React. ``` The balance in that output was read and rendered **on-chain** during step 3 — there is no `getBalance` call in your code. That is the difference between a ChainRender app and a viem dashboard. ## Nuance: three things the ethos does NOT mean (avoid over-compliance) Understanding "the component is the app" is the first step. Over-applying it produces an app that is rigid and barely functional. Three clarifications: ### A client-side resolver is NOT Grade A — an on-chain resolver contract is Routing data through a resolver does **not** make it on-chain. There are two kinds of resolver, and they grade differently: - **Client-side resolver** (a JS function you write that calls `publicClient.getBlock()` / `getBalance()`, e.g. the `makeProviderResolvers` helpers) — this is the **CLIENT_FETCH** path. The data is fetched off-chain in the browser and dropped into the slot. It is convenient and legitimate, but it grades **C (Live)**, not A. Writing `blockHeight: async () => publicClient.getBlock()` is still an off-chain fetch — moving it into a resolver function does not make it trustless. - **On-chain resolver contract** (a resolver **address** in the Slot, e.g. ``) — the value is read **and rendered on-chain** by that contract during the `eth_call`. This is the **Grade A** path. - **A viewer read or a transfer history** (`"$viewer"`, bind `transfers`; [Viewer Reads](../guides/viewer-reads.md)) is neither. The app supplies no code and no address: the viewer calls *the chain's published* resolver (or reads the chain's logs), cross-checked across its endpoints, after the person opts in. The value is still filled in the browser and is not part of the attested render, so it is **⚙ Above-floor**, never A, and it is not C either, because nothing the app wrote fetches it. The floor ("Connect your wallet to see this") is graded on its own. So: for real trustlessness, bind a Slot to an **on-chain resolver contract address**, not a client-side JS function. Client-side resolvers are a pragmatic fallback (some data, like an arbitrary wallet's ENS name, is inherently client-side) — use them knowingly, and let the grade tell the truth (they land at C). ### Ordinary client-side UI is fine — misrepresenting off-chain data as on-chain is the violation The ethos does **not** forbid all React state. A unit converter that does arithmetic in the browser, form input state, tab switching, a wallet-connect button — these are ordinary client concerns and are perfectly fine as normal React/HTML. You do **not** need to turn a client-side calculator into an on-chain transaction to be "compliant." The actual violation is narrower: **fetching on-chain data with viem and hand-rendering it as if it were rendered on-chain**, while claiming the app is fully on-chain. Pure client-side computation isn't on-chain data and never claimed to be — so it's not a violation. Don't contort ordinary UI into on-chain submissions; just don't dress up off-chain data as on-chain. ### Rendering requires a registered component — that's by design, not a bug `renderComponent` / `` render **by hash via an `eth_call`**, so the component must be **registered on-chain first**. There is no "render from raw source" — that's the whole point (the chain is the renderer). During authoring you preview with `grade()` / `compileComponent()` and the visual builder; to see the *rendered* component you register it (once) and then it renders for free forever. So "I have to register before the preview shows anything" is correct behavior, not a defect — budget one cheap registration per component. ## The one idea to internalize **A ChainRender app is a component that is compiled to bytes, registered on-chain, and rendered by an `eth_call`.** The component *is* the app. Your surrounding code (React, plain HTML, whatever) is a thin shell that calls `renderComponent(...)` and displays the on-chain HTML it returns — it does not re-implement the UI. If your React code fetches data with viem and hand-renders it in JSX, and ChainRender is only used to print a hash on the side, **you have not built a ChainRender app.** You have built a conventional Web3 dashboard that imports ChainRender. The rendering is happening in your browser, not on-chain. ## The correct flow: compile → register → render ```ts import { compileJsx } from "@chainrender/client"; import { renderComponent } from "@chainrender/client/runtime"; // 1. COMPILE — turn your component into on-chain bytes + a content hash. const { hex, hash } = compileJsx(` Account Balance `); // 2. REGISTER — store the bytes on-chain, ONCE. (Uses your wallet + the registry RenderRouter.registry() returns.) // After this, the component lives on-chain forever, addressed by `hash`. // (See the registry helpers; this is a one-time write, not part of rendering.) // 3. RENDER — anyone renders it for free with a single eth_call. The HTML is computed ON-CHAIN. const html = await renderComponent({ rpc, router }, hash); // `html` is the fully-rendered component, including the live balance the Slot resolved on-chain. document.getElementById("app").innerHTML = html; ``` Step 3 is the whole point: the balance in that `Slot` is **read and rendered on-chain** during the `eth_call`. You do **not** fetch the balance yourself and paste it into JSX — the on-chain interpreter does it, and the result is identical against any RPC, with no server. ## The anti-patterns (what "wrong" looks like) These are the exact mistakes that turn a ChainRender app into a conventional dashboard. Avoid all of them: ### ❌ Fetching on-chain data with viem and hand-rendering it ```tsx // WRONG — this is a normal React app, not ChainRender. const bal = await publicClient.getBalance({ address }); // off-chain fetch return
{formatEther(bal)} ETH
; // hand-rendered in React ``` ```tsx // RIGHT — declare a Slot; the on-chain interpreter reads + renders the balance. compileJsx(``); // → register → renderComponent ``` The difference: the wrong version renders in your browser (nothing on-chain); the right version renders on-chain and returns HTML. If you're calling `publicClient.getBalance`/`readContract` and dropping the result into JSX, you've bypassed ChainRender entirely. ### ❌ Compiling but never registering `compileJsx` gives you a hash of bytes that **do not exist on-chain yet**. A hash of un-deployed bytes is not an on-chain app — it's a preview. A real ChainRender app **registers** the bytes (step 2) so `renderComponent` can find them. If you only ever call `compileJsx` and display the hash, nothing is on-chain. ### ❌ Importing the render component but never rendering with it Importing `ChainRenderView` / `renderComponent` and then only displaying `compiled.hash` as text — with the render component absent from your returned JSX — means **nothing renders**. The app becomes a compiler-output viewer with no live UI and no interactivity. You must actually place `` in your JSX (and register the hash on-chain), or call `renderComponent` and insert the returned HTML. Importing it is not using it. ### ❌ Displaying the hash instead of rendering the component Showing "Content Hash: 0x…" in a sidebar is a nice debug view, but it is not using ChainRender. The app should **render the component** (step 3) and show *that* — the actual on-chain-rendered UI — as the primary content. ### ❌ Reaching for an off-chain indexer/API Fetching contract source from a block-explorer API, prices from a REST endpoint, or data from any Web2 service re-introduces the off-chain trust ChainRender exists to remove. If you need on-chain data, read it with a [resolver](./resolvers.md) inside a `Slot`, so it's rendered on-chain and grades honestly. If you genuinely need an off-chain source, it must be declared (`provenance: "external"` / `CLIENT_FETCH`) so the [grade](./trust-grade.md) discloses it — it cannot silently masquerade as on-chain. ## The litmus test Ask these about your app. A genuine ChainRender app answers "yes" to all: 1. **Is the UI a compiled ChainRender component** (not hand-written React/HTML that mirrors it)? 2. **Are the component's bytes registered on-chain** (not just compiled to a hash)? 3. **Is the UI produced by `renderComponent` / an `eth_call`** (not assembled in the browser)? 4. **Is on-chain data read via `Slot` + resolver** (not fetched with viem and pasted in)? 5. **Does the [trust grade](./trust-grade.md) reflect reality** (off-chain dependencies declared, not hidden)? If any answer is "no," you've built a conventional Web3 app that *uses* ChainRender, not a ChainRender app. That's a valid thing to build — but it is not on-chain, and it must not be presented as such. ## Where React fits (correctly) React is welcome — as the **shell**, not the renderer. The correct React usage: a component calls `renderComponent`, gets on-chain HTML, and displays it (see [Using React](../guides/using-react.md) and the [`@chainrender/client/react`](../reference/sdk-client.md) helpers). React handles routing, wallet connection, and chrome; ChainRender handles the actual UI, rendered on-chain. React should never re-implement what the component renders. ## Related - **[The Render Pipeline](./render-pipeline.md)** — the compile → register → render path in detail. - **[Render a Component](../guides/render-component.md)** — the one-call render, hands-on. - **[Resolvers](./resolvers.md)** — read on-chain data into a `Slot` (instead of fetching with viem). - **[Trust Grade](./trust-grade.md)** — why off-chain dependencies must be disclosed, not hidden. - **[Overview & Mental Model](./overview.md)** — the foundation. --- # No Hosting: How ChainRender Actually Works A ChainRender app has **no host**. There is no web server, no S3 bucket, no IPFS pin, no CDN — nothing that anyone has to keep running for your app to exist and work. This page explains how that's possible and why it's different from every other approach. ## Where a normal app lives vs. where a ChainRender app lives A normal web app is files (`index.html`, JS, CSS) sitting on a server. Someone pays for that server, controls it, and can change or remove the files. Even a "decentralized" dApp usually works this way — the contract is on-chain, but the *frontend* is hosted. A ChainRender app is **bytes stored on-chain** (via SSTORE2) and **rendered by an on-chain `view` function**. To display it, you call `renderHash(hash)` — a free `eth_call` that returns the HTML. There is no file on a server anywhere in this picture. The app exists as long as the chain does, hosted by no one. ``` normal app: browser → your server → index.html (server can change/remove it) ChainRender app: browser → any RPC → renderHash(hash) → HTML (on-chain, immutable, hosted by no one) ``` ## Why not just use IPFS? IPFS is a real improvement over a centralized server — it content-addresses *files*, so a CID always refers to the same bytes. But IPFS solves a different, smaller part of the problem, and it has gaps ChainRender doesn't: | | IPFS-hosted frontend | ChainRender | |---|---|---| | What's addressed | The frontend *files* (a CID) | The *app itself* (a hash), rendered on-chain | | Availability | Depends on someone *pinning* the files | Guaranteed by the chain — nothing to pin | | Discovery | Off-chain (you need the CID from somewhere) | On-chain (resolve a name → the app) | | Live data | The files are static; data still needs a separate call | Data is read on-chain at render time | | Rendering | Your browser runs the fetched JS (trust the code) | The chain renders it (analyzable, graded, safe by construction) | | Verifiable trust | The CID matches the files — but the files can do anything | Graded A/B/C, enforced, actions declared | The short version: **IPFS makes the files immutable; ChainRender makes the *app* immutable, available, discoverable, data-live, and provably safe — with nothing to host and nothing to pin.** IPFS still needs someone to keep the content pinned or it disappears; a ChainRender app can't disappear as long as the chain is alive. ## What "no hosting" buys you - **Permanence.** Your app outlives you, your company, your servers. No renewal, no re-pinning, no bit-rot. - **Censorship-resistance.** There's no host to pressure, no domain to seize, no files to take down. - **Zero ops.** No deploy pipeline, no hosting bill, no uptime to maintain. Register once; it renders forever. - **Verifiability.** Anyone reproduces the exact app from the chain — trust becomes verification. ## The one honest caveat You still read *through* something — an RPC endpoint. The *app and its output* are on-chain and reproducible (nobody can change what `renderHash` returns), but *which RPC you query* is your choice, and that RPC sees your request (a metadata concern, not an integrity one). The standard viewer asks two of the chain's public endpoints for every read and compares them, so both see it; if you opt in to showing your wallet's data in an app, they also see your address ([Privacy](./privacy.md)). Set your own private or light-client RPC if that matters to you: the viewer then reads through it alone and says so. This is the only "infrastructure" left, and it's swappable and optional — not a host of your app. ## Related - **[What's Fully On-Chain](./fully-on-chain.md)** · **[Content Addressing](./content-addressing.md)** - **[Comparison to Alternatives](../resources/comparison.md)** --- # What You Can Build ChainRender scales from a single element to a full multi-view application. Understanding the levels helps you pick the right starting point. They're the same primitives at different scales — a bigger thing is a composition of smaller things. ## 1. Components The atom. A single, self-contained UI element rendered from the vocabulary — a button, a card, a chart, an identity badge, a piece of generative art. A component: - compiles to a content-addressed hash, - renders from a free `eth_call`, - can bind live on-chain data and declare a safe action, - carries its own trust grade. **Build a component when** you want one on-chain UI element — e.g. an on-chain `tokenURI` image, a live price badge, a claim button — that you can drop into an existing site or embed anywhere. ## 2. Mini-apps A composition of components with its own identity. A mini-app is content-addressed, forkable (a composition records its parent in `CompositionRegistry`), and has an aggregated capability envelope and grade. It's a self-contained, composable unit — the natural size for a focused tool (a swap widget, a governance vote card, a credential check). **Build a mini-app when** you want a small, complete, shareable app that others can discover, use, and fork — and that composes into larger apps. ## 3. Standalone (multi-view) apps A full application with multiple views and in-app navigation (tabs, wizard, master-detail, modal). A standalone app has a shell, several views, on-chain-enforced reachability (no stranded views), and a unified envelope across everything. This is a complete dApp frontend — canonical, un-spoofable, permanent. **Build a standalone app when** you want a real product frontend — the flagship swap app (Quote / Venues / Route tabs) is exactly this shape. ## 4. Distributed forms Any of the above can also be shipped as: - a **Farcaster Mini App** (via the miniapp adapter), - an **on-chain NFT** (`tokenURI` returns a ChainRender component), - an **embedded widget** (render the HTML into any page), - a **web3:// served** resource (ERC-5219/4804). The app doesn't change to take these forms — it's the same on-chain component with a distribution wrapper. ## How to choose | You want… | Build a… | Start with | |---|---|---| | One on-chain UI element | **Component** | `create-chainrender` → Single component (T1) | | A focused, shareable tool | **Mini-app** | `create-chainrender` → Mini-app (T2) | | A full product frontend | **Standalone app** | `create-chainrender` → DeFi app (T3) | | Farcaster distribution | any of the above + adapter | `create-chainrender` → Farcaster Mini App | Everything composes: components → mini-apps → standalone apps. Start small, grow by composition — you never outgrow the model. ## Related - **[Composition & Mini-Apps](./composition.md)** · **[Templates](../reference/templates.md)** · **[Scaffolding a Project](../getting-started/scaffolding.md)** ## DeFi dashboards & apps A fully on-chain DeFi frontend: render a user's LP and lending positions (including health factor / liquidation risk), live prices, pre-trade quotes, and portfolio totals — then let them swap, approve, deposit, or claim via decoded actions. Rendering is a free `view`; only signing an action costs gas. See [DeFi Capabilities](./defi-capabilities.md) for the complete guide. ## Apps with payments (incl. non-native users) Add a payment to any app with the crypto payment as the trustless floor and an optional card/fiat on-ramp as an honestly-graded off-chain enhancement — so users without crypto can still pay by card while the on-chain guarantee is never overstated. Combined with an embedded-wallet SDK (email login → invisible wallet) and a paymaster (sponsored gas), non-native users can onboard end to end. See [Payments](./payments.md). ## RWA (real-world asset) dashboards A fully on-chain frontend for tokenized real-world assets: render Proof-of-Reserve coverage (reserves vs. supply), ERC-3643 (T-REX) compliance state (verified / frozen / can-transfer), and issuer-bound credential lifecycle (valid / expired / revoked) — each honestly graded, and compliance shown, never enforced by ChainRender. The reserve read is Grade A; a compliance verdict is a faithful render of a mutable authority (≤ C), labeled as such. See [RWA Capabilities](./rwa-capabilities.md). ## Reputation & social apps Render an address's on-chain reputation: its EFP social graph (does this list follow / tag / block / mute a target — forward reads, Grade A), delegated vouches under a caller-chosen topic (`rep-vouch` / a transparent k-of-n `rep-tally`), and a cross-source aggregate (`rep-aggregate`) whose grade is capped at its weakest input. ChainRender reads and renders reputation — it never mints a score, ranks addresses, or blesses an attester. Follower counts and reverse lookups are off-chain (indexer) and graded ≤ C, never presented as an on-chain fact. See [Reputation Capabilities](./reputation-capabilities.md). ## Identity & profile apps Build an identity UI over live on-chain sources: ENS profiles (forward-verified reverse resolution, records, ENS-v2), EAS attestations, ZK selective-disclosure credentials (member-of-set, bound single-use), and **ERC-725Y / ERC-735 account** claim reads (the standard behind LUKSO Universal Profiles) via `staticread` today — read against a **co-located** account (same chain the resolver is deployed on); a typed reader is planned. LUKSO's own chain is **not integrated as of now** — a live LUKSO Universal Profile would require ChainRender deployed on LUKSO; today's reach is same-chain ERC-725Y/735 accounts. Owned/mutable identity sources render as "the current claim of authority Y" (≤ C), never as proof — the honest boundary throughout. See [Reputation Capabilities](./reputation-capabilities.md#erc-725y--erc-735-accounts) and [Privacy & Credentials](./privacy.md). --- # Content Addressing Content addressing is the foundation everything else in ChainRender rests on. Understand this one idea and the rest of the system follows naturally. ## The core idea A ChainRender app **is** its hash. When you compile a component, you get a `keccak256` hash of its exact bytes. That hash is the app's permanent, global identity. To render it, you call `renderHash(hash)` — a `view` function that deterministically produces the same output every time, forever. ``` compile(component) → bytes → keccak256(bytes) = the app's identity renderHash(hash) → always the same HTML ``` There is no "current version" pointer that someone can change, no server that can serve different content, no mutable state in the render path. **The hash and the output are bound together immutably.** ## Why this matters: the anti-spoofing guarantee Consider a claim button that says "Claim 1 ETH" and calls `claim(uint256)` with `1000000000000000000`. In a normal web app, the server could serve you a *different* button — same label, but calling `claim` with a larger amount, or calling a different contract entirely. You'd have no way to know. With content addressing, that button's bytes — its label, its target, its selector, its arguments — are all part of the hash. **Change any of them and the hash changes.** A viewer that wants to show you a malicious variant cannot do so under the same hash; it would be a *different* app with a *different* identity. If you (or your wallet) pin to a known-good hash, you are guaranteed the exact app you expect. This is the same guarantee that content addressing gives IPFS or Git — but applied to a *live, interactive, transaction-signing UI*, enforced on-chain. ## Why it's built this way (the design reasoning) The alternative — a mutable app that an owner can update in place — was deliberately rejected for the base layer, because a mutable pointer is precisely the trust surface content addressing exists to eliminate. If "app.eth" could be repointed at any bytes instantly, whoever controls that name could swap the app under its users. That's the problem, not the solution. So ChainRender makes **immutability the default and the floor**, and layers *controlled, observable* mutability on top only where it's safe: - **Presentation** (how it looks) is a render-time parameter — changeable at zero gas *without* changing the hash, because appearance doesn't affect what the app *is* or *does*. See **[Theming](./theming.md)**. - **Versioning** (evolving what the app is) is handled by an explicit, timelocked, append-only version registry — so an update is a *public, delayed, immutable event*, never a silent swap. See **[Versioning](./versioning.md)**. The rule that ties it together: **how an app looks is free and mutable; what it *is*, *says*, and *does* is baked into its hash.** That division is what lets ChainRender be both immutable (trustworthy) and evolvable (usable). ## The practical consequences for you - **Deploying is registering bytes.** You compile locally, get a hash, register the bytes once (paying gas once), and render for free forever after. - **The same app renders identically on any chain.** Because rendering is deterministic and content-addressed, an app deployed to multiple chains has the same hash and same output everywhere. - **You can verify any app.** Given a hash, recompile the source and check it matches; render against any RPC and check the output matches. Nothing is hidden. ## Related - **[The Render Pipeline](./render-pipeline.md)** — what `renderHash` actually does. - **[The Trust Grade](./trust-grade.md)** — how deviations from pure content-addressing are measured and disclosed. - **[Versioning & Updates](./versioning.md)** — safe, observable app evolution over immutable targets. --- # The Economics: Write Once, Read Free Forever ChainRender has an unusual and powerful cost model: you **pay gas once to store an app, then render it for free, forever.** This page explains the model, the numbers, and why it works — because it's one of the strongest reasons to build on-chain UIs at all. ## The core model | Action | Cost | When | |---|---|---| | **Register app bytes** | gas, once | at deploy | | **Render the app** | **free** | every time, forever | | **Read live data** | **free** | every render | | **Change the theme/look** | **free** | render-time | | **User signs an action** | normal tx gas | only when *they* act | The asymmetry is the point: **storage is a one-time write; rendering is an unlimited free read.** ## Why rendering is free Rendering is a `view` function (`renderHash`) — an `eth_call`. Reads don't change state, so they cost no gas. Whether the interpreter does a little work or a lot, whether one person renders it or a million, whether it renders today or in ten years — **it's always free to the user.** There is no per-render cost, no metering, no rate to pay. > A common point of confusion: an RPC provider may cap the *work* a single `eth_call` can do (a gas cap on the > call), but that's a provider-side limit, **not a cost to the user**. And ChainRender's paginated reads handle > apps that exceed any single-call limit. The user never pays to render. ## Why storage is a one-time write App bytes are stored via **SSTORE2** — a pattern that stores data as contract *code* rather than in storage slots. This is dramatically cheaper than naive `SSTORE`, and once written, the bytes are immutable and read cheaply (via `EXTCODECOPY`) forever. You pay the write once; every future render reads those same bytes for free. Storage runs on the order of **~216–226 gas per byte** on chains without Ethereum's Glamsterdam repricing, and **~1,559 gas per byte** on chains with it (measured on Sepolia, October 2026: ~7.2× more; Ethereum mainnet has not scheduled it yet). So a component's one-time deploy cost scales with its size and with the chain you deploy to — and after that, it renders for free indefinitely. There's no recurring cost, no renewal, no re-upload. ## Concrete numbers - **A small component:** registering one measured **~189,000 gas** for a 17-byte component (most of it fixed registry bookkeeping), then free renders forever. Measured before Glamsterdam; that bookkeeping is part of what the repricing raised. - **A large on-chain library** (p5.js + three.js core, gzipped, ~83M gas): about **$20 at 0.1 gwei** (~$200 at 1 gwei) to pin once, before Glamsterdam pricing — a one-time cost shared by every scene that references it. Roughly 7× that on a chain with the repricing. - **The framework itself:** a one-time deploy, measured at **82.5M gas** for the original 46-contract set on a local pre-Glamsterdam chain (the full set has since grown). That's *infrastructure*, deployed once by the operator, not something an app developer pays. You deploy your *app* against the live framework. (Gas figures are indicative and depend on size, chain and gas price. Estimate with `scripts/compare-deploy-cost.mjs`, which uses measured numbers per chain where they exist, before any mainnet deploy.) ## Why this changes the calculus A traditional web app has *ongoing* costs — hosting, a domain, a CDN, maintenance — that never end and that someone must keep paying or the app disappears. ChainRender inverts this: - **No recurring cost.** Pay the one-time write; there's nothing to renew. - **No hosting bill.** There's no server (see **[No Hosting](../concepts/no-hosting.md)**). - **Cost scales with size, not usage.** A popular app costs the same as an unused one — because rendering is free. A million users cost you nothing. - **Permanence is paid-for, once.** The app exists as long as the chain does, with no further payment. This is what makes on-chain UIs viable as **public goods**: an app can be deployed once and serve everyone, forever, with no one needing to keep funding its existence. ## The one-time costs, summarized You pay gas once for each of these, then never again: 1. **Register your app's component bytes** (the app itself). 2. *(Optional)* **Register a subname** (a refundable stake, not a fee — you get it back). 3. *(Optional)* **Seed a custom theme** on-chain (once, if you commit one). 4. *(Optional, operator-level)* **Pin a rich-media library** (p5/three) if your app uses one. Everything else — rendering, reading live data, applying themes, verifying — is free, forever. ## Related - **[No Hosting: How It Works](../concepts/no-hosting.md)** — why there's no recurring cost. - **[Content Addressing](../concepts/content-addressing.md)** — the immutability that makes read-once-store-forever work. - **[Deploying an App](../guides/deploying.md)** — the one-time write in practice. --- # What's Fully On-Chain (Capabilities & Robustness) ChainRender's central claim is that apps are **fully on-chain**. This page substantiates that claim: it catalogs *exactly* what lives on-chain, *what capabilities* that unlocks, and *what engineering makes it robust*. If you're evaluating whether "fully on-chain" is real or marketing, this is the page to read. ## What is actually on-chain Everything required to store, render, and verify an app — and to read the live data it displays: | On-chain | What it is | |---|---| | **App bytes** | Your compiled component tree, stored via **SSTORE2** in the `ComponentRegistry`. | | **The renderer** | `RenderRouter` + the interpreter — a `view` function that turns bytes into HTML/SVG. | | **The vocabulary** | Every UI primitive (box, text, button, chart, table, controls, layout, scene) is rendered by on-chain code. | | **Live data** | Resolvers (`view` contracts) read chain state at render time — prices, balances, ENS, credentials. | | **Media** | Generative art, SDF/point-cloud scenes, NFT images, geo maps, identicons, gradients — all generated on-chain as SVG. | | **Rendering libraries** | p5.js, three.js (r160 UMD), and the ~500-icon set are stored **on-chain** in the `DependencyRegistry` (gzip-compressed, fetched by name, decompressed natively in the browser). Rich-media apps and icons load their libraries from chain — **never from a CDN**. | | **The trust logic** | The enforcer + trust grade compute on-chain. | | **Theming** | Design tokens → CSS generated on-chain (`ThemeCssView`); themes stored on-chain. | | **Naming & versions** | Subname registrar, version registry, theme sets, advisories — all on-chain. | There is **no server** in the render path, **no `index.html` on a host**, **no build pipeline**, and **no CDN — not even for the rendering libraries** (p5/three/icons load from chain, as plain scripts, decompressed natively; an embedded copy is only a fail-open fallback). To display an app you make one `eth_call`. To verify it you recompile and re-render — nothing is hidden off-chain. ## What this unlocks (capabilities) Because it's all on-chain, ChainRender apps get properties a hosted frontend cannot: - **Permanent.** The app exists as long as the chain does. It cannot be taken down, and it doesn't depend on anyone continuing to host it. - **Un-spoofable.** Content addressing binds the exact UI to its hash. No one can serve you a different button under the same identity. - **Reproducible.** Anyone renders the same app against any RPC and gets byte-identical output. Trust becomes verification, not faith. - **Chain-portable.** Deterministic rendering means the same app has the same hash and output on any EVM chain. - **Free to render.** Rendering is a `view` call — no gas. You pay once to register bytes; render forever for free. - **Rich.** Charts, tables, multi-view navigation, 2D/3D scenes, generative art, animation, live data, themes — the on-chain surface is broad, not a lowest-common-denominator "static token image." ## What makes it robust (the built-in engineering) "Fully on-chain" would be fragile if a single failure could break a render or produce a lie. ChainRender is engineered so that cannot happen. These are the robustness mechanisms, and they're not aspirational — they're built and tested: - **Fail-closed everywhere.** A reverting resolver, a gas-bomb, a missing source, a failed proof, an unavailable GPU — none breaks the render. Each degrades to an honest fallback: "unavailable," an axes-only chart, a poster image. An app can never be *made* to render a lie by a failing dependency. - **The floor.** Every app has a complete, on-chain rendering that needs nothing but the chain; enhancements (GPU, fetch, motion) layer on top and fail closed to it. See **[The Floor](./the-floor.md)**. - **Escaped by construction.** Every value a resolver returns is re-escaped before entering the output — a resolver can never inject markup. Safety is structural, not a check you might forget. - **Drift-locked.** The SDK's off-chain logic (grade, format constants, theme generation) is asserted *byte-identical* to the on-chain logic by tests. What you compute locally is exactly what the chain computes — no divergence. - **Paginated reads.** Large outputs (a world atlas, a long table) support chunked reads that concatenate byte-identically to the whole, so even big apps stay within call limits without truncation. - **Gas-bounded resolution.** Data slots resolve under a gas stipend; a resolver that tries to run away is cut off and the slot fails closed — one expensive read can't break the page. - **Legibility & accessibility invariants.** A font-size floor (never below 12px) and a WCAG contrast invariant are enforced as build-failing checks — an illegible or inaccessible theme cannot ship. - **Content-addressed storage.** SSTORE2 stores bytes immutably and cheaply; the same bytes always yield the same hash, so storage itself is verifiable. - **Ownerless & immutable contracts.** No admin, no upgrade key anywhere in the render path — nothing can change what your app displays after you verify it. ## The honest boundary Two things are worth stating plainly, because ChainRender's discipline is to never overclaim: 1. **The read path is your choice.** The *rendered output* is fully on-chain and reproducible, but *which RPC you read through* is a client decision — an RPC sees which app and address you queried (a metadata concern, not an integrity one). You can use a private or light-client RPC to address this. 2. **Declared off-chain enhancements exist and are disclosed.** An app *may* declare a `CLIENT_FETCH` (off-chain data) — but doing so **drops its grade** and is itemized in the breakdown. The floor beneath it stays on-chain; the enhancement is never hidden. Neither of these weakens the core claim — they *sharpen* it. "Fully on-chain" means the app's identity, rendering, and integrity are on-chain and verifiable; where something optional reaches off-chain, the grade says so. ## Related - **[The Floor](./the-floor.md)** — why enhancements never compromise the on-chain guarantee. - **[The Render Pipeline](./render-pipeline.md)** — the fail-closed render path, step by step. - **[The Trust Grade](./trust-grade.md)** — how any deviation from fully-on-chain is measured and disclosed. - **[Contracts → Architecture](../contracts/architecture.md)** — the on-chain contracts, by layer. --- # The Render Pipeline What actually happens when an app renders, start to finish. ## The path ``` renderHash(hash) → the registry reads the app's bytes (stored on-chain via SSTORE2) → the interpreter walks the byte-encoded component tree → for each node: emit its HTML/SVG from the closed vocabulary → for each data slot: call the bound resolver (a view call) and inline the result (escaped) → for each image slot: resolve on-chain media (or fall back to a poster floor) → return the assembled HTML string ``` Every step is a `view` — no state changes, no gas, fully reproducible. ## Key properties - **Deterministic.** The same hash always yields the same output. This is what content addressing guarantees. - **Fail-closed.** A reverting resolver, a missing source, a gas-bomb, or an unknown node never breaks the render — it degrades to an honest fallback ("unavailable", an axes-only chart, a poster image). An app can't be made to render a lie by a failing dependency. - **Escaped by construction.** Any value a resolver returns is re-escaped before it enters the output — a resolver can never inject markup. Safety isn't a runtime check you might forget; it's structural. - **Paginated when large.** Big outputs (an atlas, a long table) support chunked reads that concatenate byte-identically to the whole — so even large apps stay within call limits. ## Two render entry points - `renderHash(hash)` — the plain render; baked defaults. - `renderHashWithContext(hash, ctx)` — the same app with a **presentation context** (theme, palette, chart dimensions) applied at render time. Same app, same hash, different *appearance* — zero gas, no redeploy. See **[Presentation & Theming](./theming.md)**. ## Related - **[Resolvers & Data Binding](./resolvers.md)** — where live data comes from. - **[The Trust Grade](./trust-grade.md)** — how the pipeline's dependencies are graded. --- # Resolvers & Data Binding A resolver is an on-chain contract that supplies **live data** to your UI at render time. It's how a ChainRender app shows real, current chain state — a price, a balance, an ENS profile, a chart series — while staying fully on-chain and grade-A. ## The idea Your composition has **data slots**. Each slot names a resolver and a "kind" of data. When the app renders, the interpreter calls `resolver.resolve(kind, args)` — a `view` call — and inlines the (escaped) result. The data is never baked into the app's bytes; it's read live, so the same app always shows *current* state. ```ts // a slot bound to a live oracle price compose.slot("feed", { resolver: "0x…", arg: encodedArgs }) // slot(bind, { resolver, arg }) ``` ## Why resolvers instead of baking data in If data were baked into the app's bytes, the app would show *stale* data forever (and every update would be a new hash). By reading through a resolver at render time, the app stays immutable *and* current — the bytes say "show the price from this feed," and the price itself is always fresh. This is the separation of **structure** (immutable, in the hash) from **state** (live, from resolvers). ## The resolver interface Every resolver implements a tiny interface: `supportsKind(kind)` (a capability check) and `resolve(kind, args)` (the read). This uniformity is what lets the interpreter treat all data sources identically, and lets you swap one resolver for another without changing your UI. Some resolvers also implement richer interfaces (`IChartData`, `ITableSource`, `IRouteSource`) for structured data. ## Fail-closed, always A resolver that reverts, runs out of gas, or returns garbage never breaks the render — the slot falls back to an honest "unavailable." A resolver can never inject markup (its output is escaped) and never move value (reads are `view`). Binding live data adds a `READ_CHAIN` capability and nothing more. ## The built resolver catalog ChainRender ships a large library of ready resolvers — oracles (Chainlink, Uniswap V3), DeFi (Aave, protocol stats), identity (ENS profile, attested credentials), media (NFT images, generative art, SDF scenes, geo maps), and structured data (charts, tables). See the **[Resolver Catalog](../reference/resolvers.md)** for a tour, and **[All Resolver Kinds](../reference/resolver-kinds.md)** for every first-party kind with its exact args — the same list `vocabulary().resolverKinds` returns. **Encode the args the way the kind says.** A slot's `arg` is the bytes the resolver receives: `abi.encode` of the kind's args, unless the kind is marked *packed* (raw bytes — `ensName` takes exactly the 20 address bytes and reverts on anything else, which falls back to the slot's text). A wrongly encoded arg doesn't error at build time; it renders the fallback. ## Related - **[Guides → Bind Live On-Chain Data](../guides/bind-data.md)** — a hands-on walkthrough. - **[The Trust Grade](./trust-grade.md)** — how a live read affects the grade (spoiler: an on-chain read stays A). --- # DeFi on ChainRender — Complete Capability Guide ChainRender can build **fully on-chain DeFi frontends**: apps that render live market data, a user's actual positions, and pre-trade quotes, then let the user act — all as `eth_call` renders plus declared, decoded actions. This page is the comprehensive reference for what ChainRender can and cannot do with DeFi, how each piece works, and the honest boundaries. ## The mental model: render + declare, never *become* the protocol ChainRender does two things for DeFi, and deliberately not a third: 1. **Render on-chain DeFi state** — prices, positions, quotes, approvals, pool state — as a free `view` call. No server, no indexer; the data is read from the protocol's own contracts during the render. 2. **Declare DeFi actions** — a button carries a declared `{to, sig, value, args}` (a swap, mint, approve, deposit), decoded into plain English before the user signs, governed by the capability envelope. 3. **It does NOT become a DEX / lending protocol / bridge.** ChainRender renders and triggers DeFi; it never holds funds, runs an AMM, or custodies anything. An app *built with* ChainRender can present a Uniswap swap — but the swap executes on Uniswap, not on ChainRender. This separation is the whole safety story: rendering is trustless (a `view`), and actions can only do exactly what they declare (enforced by the capability system + the action decoder). ## What ChainRender can RENDER (the resolver suite) Each resolver is an immutable, ownerless, **view-only, fail-closed** contract: a reverting/missing target renders `"unavailable"` rather than breaking the page. An app references a resolver by address + kind in a slot; the interpreter calls `resolve(kind, args)` at render time. ### Market data | Resolver | Kind | Renders | |---|---|---| | ChainlinkFeedResolver | `price` | An AggregatorV3 oracle price with freshness/staleness | | UniswapV3PriceResolver | `uniV3Price` | Live V3 pool spot price (slot0 sqrtPriceX96) | | UniswapV4PriceResolver | `uniV4Price` | Live V4 pool spot price (StateView.getSlot0) — additive to V3, both kept | | AaveReserveResolver | `aaveSupplyApy` | Aave reserve rates (supply APY etc.) | | CurvePoolResolver | `curvePool` | Curve pool get_dy quote + balances (stable pools) | ### A user's positions (the qualitative jump — not just prices, *your* holdings) | Resolver | Kind | Renders | |---|---|---| | UniswapV3PositionResolver | `uniV3Position` | An LP NFT: pair, fee tier, tick range, liquidity, uncollected fees | | AavePositionResolver | `aavePosition` | A lending position: collateral, debt, borrowing power, **health factor** (liquidation risk; `∞` when no debt) | ### Pre-trade + flow control | Resolver | Kind | Renders | |---|---|---| | UniswapQuoteResolver | `uniQuote` | Expected swap output ("swap X → receive ~Y") before signing, via QuoterV2 | | ApprovalStateResolver | `approvalState` | Whether a spender is approved for enough of a token — gates "Approve" vs "Swap" | | PortfolioResolver | `portfolio` | An aggregated **total** summing many numeric reads (portfolio value across positions) | ### Generic (any protocol, no purpose-built resolver needed) | Resolver | Kind | Renders | |---|---|---| | CallResolver | `staticread` | Any contract's view function — read any DeFi protocol, hook, or token | | MultiCallResolver | `multiread` | Several reads joined side by side (a dashboard row) | So for a protocol ChainRender doesn't ship a dedicated resolver for (Balancer, Compound, a new AMM), `staticread` reads it generically — the purpose-built resolvers just add nicer formatting for the common cases. ### Intent orders (CoW Protocol) Beyond spot swaps and positions, ChainRender renders **intent-based** DeFi — CoW Protocol's programmatic/conditional orders — and verifies the document a user is about to sign, all as trustless `view` reads: | Resolver | Kind | Renders | |---|---|---| | CowTwapResolver | `cowTwap` | a TWAP order's live schedule (part X/N, next-slice countdown, per-part limit) from its definition + `block.timestamp` | | CowConditionalOrderResolver | `cowOrder` | live tradeability this block via a STATICCALL to the real **ComposableCoW**, fail-closed to `"not active"` | | CowIntentVerifyResolver | `cowIntent` | on-chain `keccak256(fullAppData) == order.appData` → `"verified"` / `"app-data mismatch"` — "verify what you sign" | These render the on-chain truth of an intent order; ChainRender never becomes the solver or the off-chain batch auction. Full treatment, including the ⚙ client hook decoder, in **[Intent DeFi & CoW Protocol](./intent-defi-cow.md)**. ## What ChainRender can DECLARE (the action layer) A DeFi action is a button with `action: {to, sig, value, args}`: - **Swap** — `to` = router, `sig` = `swapExactTokensForTokens(...)` (or the V4/universal-router swap) - **Approve** — `to` = token, `sig` = `approve(address,uint256)` - **Deposit / withdraw / borrow / repay** — the protocol's method - **Mint / claim / stake** — the same shape The action layer enforces honesty: - **Capability envelope** — a write action requires the app to declare `WRITE_CHAIN`; sending value requires `TRANSFER_VALUE`; batching (approve+swap) requires `BATCH_EXECUTE`; EIP-712 permits require `SIGN_MESSAGE`. The validator *rejects* an app whose actions exceed its declared capabilities, and rejects over-declaration too. - **Bound interfaces** — an action can only call a `sig` on a `to` that the app manifest declares. No smuggled calls. - **The action decoder** — before signing, the user sees a plain-English decode: *"Swap 1.0 WETH → 0x…, min out 2950 USDC"*. Swaps carry an **honesty flag** because the raw params don't self-label the slippage minimum or the token path — so the decode tells the user to verify those. ## The honest, complete DeFi swap flow Putting it together, a swap app on ChainRender does the honest thing end to end: 1. **Render the quote** — `uniQuote` (or `curvePool` get_dy): "you'll receive ~2950 USDC". 2. **Render approval state** — `approvalState`: show "Approve" if the router isn't approved, else "Swap". 3. **Declare the action** — the swap button, with a **slippage minimum** the user controls. 4. **Decode before signing** — the action decoder shows exactly what's being signed, with the swap honesty flag. 5. **Execute** — via the wallet (`eth_sendTransaction`); the swap runs on the DEX, not on ChainRender. Every step is visible, every risk is flagged, and nothing is claimed beyond what the code enforces. ## The honest boundaries (read these) - **Quotes and get_dy are ESTIMATES.** They reflect current pool state; the actual output can differ because price moves between the quote and execution. This is exactly why the swap action must still carry a slippage minimum — the resolver shows the expected output, it does not guarantee it. - **Rendering is free; only *acting* costs gas.** A DeFi dashboard that only renders positions/prices costs the user nothing — it's a `view`. Gas is only spent when the user signs an action (swap, approve, deposit). - **The resolver needs the protocol to exist on the target chain.** A resolver is correct code, but it can only return data if the Uniswap/Aave/Curve instance it points at exists on the chain you're on. See [Sepolia DeFi Addresses](./sepolia-defi-addresses.md) — Chainlink/Aave/Uniswap V3 exist on Sepolia; Curve is thin there and may need an L2/mainnet to test live. - **ChainRender blesses no protocol.** The resolvers take the pool/feed/pool address as a caller argument — ChainRender doesn't endorse any oracle, DEX, or lending market; the app author chooses, and the user verifies. - **No cross-chain position aggregation yet.** Rendering a user's positions *across* chains needs trustless state proofs. That machinery is live for **token balances**: [Proven Balances](./cross-chain-balances.md) proves a Sepolia token's balance on Sepolia and, since Milestone 2, on Robinhood Chain Testnet, against Sepolia's own state. Since Milestone 3 it also covers **already-deployed** tokens — a snapshot of the token's own `balanceOf` (`xchain-snapshot-balance`) or an adoption claim about its storage layout (`xchain-adopted-balance`, always ≤ C) — and Milestone 4 proves a hash-timelock swap lock (`swap-lock`). Multi-protocol *position* aggregation (LP shares, lending positions) is not built. Per-chain DeFi reads work today. ## What's NOT built (honest gaps) - **Balancer resolver** — `staticread` reads Balancer's Vault generically today; a purpose-built one is a future add. - **Uniswap V4 LP position** — V4's singleton architecture differs; the V4 *price* resolver exists, a V4 *position* resolver is a follow-up. - **Curve crypto-pool (uint256 index) variant** — the shipped `curvePool` targets classic int128 stable pools. - **Cross-chain position state** — not built. Cross-chain reads are live for token balances (Sepolia → Robinhood Chain Testnet, [Proven Balances](./cross-chain-balances.md)); DeFi positions across chains need per-protocol position adapters, which are designed (doc 175 §11.3) but not built. ## Trust grade for DeFi apps DeFi reads are on-chain `view` calls, so a DeFi app that renders positions/prices/quotes grades **A (Trustless)** on its data edge — the data is read from the protocol's own contracts, no off-chain dependency. Actions don't lower the render grade (rendering stays a `view`); they're governed separately by the capability envelope. An app that pulled a price from an *off-chain* API instead would grade ≤ C — but the ChainRender resolvers all read on-chain, so they stay A. ## Payments Beyond swaps and positions, ChainRender apps can take **payments** — a crypto payment (trustless, Grade A) plus an optional card/fiat on-ramp (honestly graded off-chain) so non-native users can pay by card. See [Payments](./payments.md). > All the resolvers on this page are now deployed on **all three testnets** (Sepolia, Robinhood, Circle Arc), and > they're **caller-supplied-address** — they read whatever pool/position/token the caller points at, and fail closed > to `"unavailable"` if it isn't there. ## Where to go next - **[Intent DeFi & CoW Protocol](./intent-defi-cow.md)** — render + verify CoW conditional/TWAP orders and "verify what you sign". - **[Resolvers](./resolvers.md)** — how the resolver mechanism works in general. - **[RWA Capabilities](./rwa-capabilities.md)** — Proof-of-Reserve, ERC-3643 permission display, credential lifecycle. - **[Reputation Capabilities](./reputation-capabilities.md)** — EFP social graph, delegated vouching, aggregate, ERC-725Y/735 account reads (same-chain). - **[Integrations & Libraries](../reference/integrations.md)** — every integration + library + SDK/skill/MCP at a glance. - **[Actions & Signing](./actions.md)** — the declared-action model + the "you see what you sign" decoder. - **[Rendering Uniswap Hooks](./rendering-uniswap-hooks.md)** — reading V4 hook state via `staticread`. - **[Sepolia DeFi Addresses](./sepolia-defi-addresses.md)** — which protocols exist on Sepolia + per-resolver readiness. - **[Trust Grade](./trust-grade.md)** — why on-chain DeFi reads grade A. - **Reference: [Resolver Catalog](../reference/resolvers.md)** — every resolver + kind + args. --- # Proven Balances (Cross-Chain) A ChainRender app can show a token balance that is **proven**, not just reported. The balance is checked against the chain's own state commitment — block header → state root → token account → the exact storage slot that holds the balance — by verifier contracts that are themselves on-chain, ownerless and immutable. The render then says precisely what was proven, when, and how strongly: ``` 1000000.00 CRPD · proven @ block 11855669 · 24s old · Grade A (on Sepolia) ``` The same balance can also be proven **on another chain**, without bridging anything. On Robinhood Chain Testnet, whose parent chain is Sepolia, the line reads: ``` 1000000.00 CRPD · proven @ block 11857149 · 781s old · Grade A (on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10) ``` **Milestone 1** is the proof stack on Ethereum Sepolia, proving balances of tokens whose home is Sepolia. **Milestone 2** delivers Sepolia block hashes to Robinhood through Robinhood's own parent-chain messaging, and the **same cache contract, unchanged**, proves Sepolia balances there (see *On another chain* below). **Milestone 3** extends this to tokens ChainRender **did not create** — any existing ERC-20, proxies included — by having the token's own `balanceOf` run on Sepolia and proving the answer (see *Tokens ChainRender didn't create*): ``` 10000.00 USDC 0x1c7d4b196cb0c7b01d743fbc6116a902379c7238 · balanceOf @ block 11859963 (in-transaction) · 798s old · Grade A (on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10) ``` > **Status: live on Sepolia and Robinhood Chain Testnet, alpha.** Every contract is source-verified: Milestone 1 on > Etherscan; Milestone 2 on Etherscan (the Sepolia sender) and on Robinhood's Blockscout explorer (the four Robinhood > contracts). Milestone 2 ran its end-to-end acceptance on 2026-10-06, Milestone 3's snapshot mode (three more > contracts, all source-verified) the same evening, and its storage-layout mode (eleven deployments, all > source-verified) on 2026-10-07. Addresses: > [Deployed Contracts](../contracts/deployed-contracts.md), *Proven balances*. ## Why prove a balance at all An ordinary dapp shows a balance by calling `balanceOf` and trusting the answer. On the token's own chain that's fine. Everywhere else it isn't: another chain can't call the token, so the number has to come from somewhere — a bridge, an oracle, an indexer — and the user is trusting that somewhere. ChainRender's rule is that **an address is a pointer, not a trust claim.** So instead of trusting a reported number, the proof stack checks it against the source chain's own commitments, and the trust grade says which commitment it checked against. ## How it works Three steps, two of them permissionless writes and one a free read: 1. **Register the token (once).** Anyone submits a proof of the home factory's record for the token. The record binds the token's **definition** (name, symbol, decimals, creator, supply), its **runtime code hash** and its **storage namespace**. The cache never asks the token to describe itself; every fact comes from the factory's proven storage. 2. **Refresh a balance.** Anyone submits a proof of a holder's balance at some block. The cache verifies the header against the root source, proves the token account (its code hash must equal the registered one), then proves the balance slot computed from the **registered** namespace. The verified value is cached with its block, the proven header's timestamp and a grade. 3. **Render (free).** The `xchain-balance` resolver is a plain `view`. It reads the cache and renders the value, its block, its age in seconds, its grade and the host chain. Proof verification never runs inside the render — a render has a small gas budget, and a proof costs hundreds of thousands of gas. Verification is strict and fails closed. A tampered proof node, a header from another block, extra proof elements, a non-canonical encoding or a token whose code differs from the record all **revert** — nothing can fail toward a wrong number. A holder with no balance is proven too (an exclusion proof), and renders `0`. ### Root sources and grades A proof is only as strong as the block hash it's checked against. That hash comes from a **root source**, and each root source sets a ceiling on the grade: | Root source | Where the block hash comes from | Grade ceiling | |---|---|---| | **Same-chain** | the chain's own `BLOCKHASH` (last 256 blocks) and its EIP-2935 history contract (about 27 hours on Sepolia) | **A** | | **Parent-chain retryable** (Arbitrum) | a parent-chain block hash delivered through the rollup's own Inbox, accepted only from one fixed sender | **A, relative to the host** — always shown with the host's facet | | **Relay** | posted by a fixed set of posters | **C** — a poster could post anything | The cache keeps a **separate entry per root source**, so a weak source can never overwrite or block what a strong one proved. The resolver shows the **highest-grade fresh entry** (ties go to the newest). Grade A means *relative to the host chain*, the chain the render runs on. On Sepolia, the value is exactly what Sepolia's consensus committed to at that block. On another chain it also trusts that chain, which is why the host's own trust is printed next to the letter (see below). It is the same letter as the [trust grade](./trust-grade.md), with the same discipline — the grade never claims more than the code checks. ### What it does not establish Every grade states what it does *not* cover. The resolver publishes this list on-chain (the `scope-disclosure` kind renders it for any contract that implements it): - anything beyond the host chain's own consensus (host-chain grade) - the balance now — only as of the proven block - anything the cache or its root source does not establish (see those) That second line is why the render always carries the block and the age: a proven balance is a fact about a block, not a promise about the present. ### Freshness Age is measured in **seconds since the proven header's timestamp**, against the host chain's current block time. The app chooses how old is too old (`maxAgeSeconds`). Past that, the render says `stale (proven @ block N · Grade G)` — never the old number as if it were current. An entry dated more than `maxAgeSeconds` into the future is not credible and renders `unavailable`. ## Using it in an app Bind the `xchain-balance` resolver kind in a data slot. Its arguments: | Arg | Type | Meaning | |---|---|---| | `token` | `address` | the home token (must be registered in the cache) | | `holder` | `address` | whose balance | | `maxAgeSeconds` | `uint64` | older than this renders `stale (…)` | | `fracDigits` | `uint8` | fractional digits to show (0–77) | The resolver's cache is fixed at deployment, so a slot can never be pointed at a self-made cache that claims Grade A. Symbol and decimals come from the **proven definition**, not from the token. The renders it can produce: | Situation | Render | |---|---| | a fresh proven entry | ` · proven @ block N · s old · Grade (on )` | | … on a non-home chain | ` · proven @ block N · s old · Grade (on · )` | | entries exist, none fresh | `stale (proven @ block N · Grade G)` | | … on a non-home chain | `stale (proven @ block N · Grade G · on · )` | | token not registered, no entry, bad arguments, or out-of-bounds display values | `unavailable` | Symbols are restricted to `[A-Za-z0-9]{1,11}` and decimals to at most 77, enforced when a token is created and checked again at every step, so a token can't smuggle separators, markup or a fake "Grade A" into the line. ### Proving and refreshing from code The proof helpers live in the repository's `chainc/xchain.ts`. They fetch `eth_getProof` and the block header from any RPC and package them for the cache; the header is RLP-encoded locally and checked against the real block hash before anything is submitted. ```ts import { buildRecordProof, buildBalanceProof, waitUntilProvable, KIND_XCHAIN_BALANCE, encodeXchainBalanceArgs, } from "./chainc/xchain.js"; // a same-chain root for block n exists only once block n + 1 does const n = receipt.blockNumber; await waitUntilProvable(publicClient, n); await cache.write.registerToken([sameChainSource, n, registration, await buildRecordProof(publicClient, factory, token, n)]); const m = (await publicClient.getBlockNumber()) - 1n; await cache.write.refresh([sameChainSource, m, token, holder, await buildBalanceProof(publicClient, token, holder, m)]); // free: the exact string a ChainRender app shows const line = await resolver.read.resolve([KIND_XCHAIN_BALANCE, encodeXchainBalanceArgs(token, holder, 3600n, 2)]); ``` `scripts/xchain/deploy-sepolia.ts` runs this whole sequence end to end (with `XCHAIN_DEMO=1`), and the chainc mirror `renderXchainBalance` produces the identical string off-chain — a drift test keeps the two equal. ## On another chain: Robinhood Chain Testnet Robinhood Chain Testnet is an Arbitrum chain whose parent is Sepolia (checked on-chain: its rollup contracts live on Sepolia and its blocks track Sepolia's head). That gives it a way to learn Sepolia block hashes that adds no trust beyond Robinhood's own: 1. **Deliver (anyone, paid by whoever wants a fresh root).** `ArbRootSender` on Sepolia reads a recent block hash from the Milestone 1 same-chain source and sends it to Robinhood as a **retryable ticket** through the rollup's Inbox. The sender is ownerless and holds nothing; the caller pays the ticket's fees in the same call. 2. **Receive.** `ArbParentRootSource` on Robinhood accepts a block hash **only** from that sender's L1→L2 alias and only for Sepolia's domain, append-only: a different hash for the same block reverts. 3. **Prove and render, as on Sepolia.** The cache on Robinhood is the **same contract code** as Milestone 1, with Sepolia as its home. It verifies Sepolia's record and balance proofs against the delivered hash. **The host facet.** Anything running on Robinhood already trusts Robinhood, including whoever can upgrade it, so the grade is A *relative to Robinhood*, and the render must never show that letter bare. The resolver carries an immutable, dated **facet** describing Robinhood's upgrade setup, read from the chain before deployment: every upgrade path (the rollup contracts on Sepolia and the chain owner on Robinhood) ends at **one Safe with a threshold of 1 of 2** — one key, no delay. The Safe has no modules or guard on either chain, so its two signers are the only ones who can act. So the line says `instant upgrades, 1-of-2 Safe, as of 2026-10`. The resolver refuses to deploy without a facet when its cache's home is another chain, and its scope disclosure adds that the facet was recorded at deployment and may have changed since. **What the delivered root does not establish** (published on-chain by `ArbParentRootSource`): anything beyond the host's consensus; that the Inbox and Bridge behave as published (they are upgradeable — see the facet); that a root arrives at any particular time; that a delivery cannot be censored (the recourse is the rollup's forced inclusion, after 4 days on this testnet); and anything the Sepolia same-chain source does not establish. **Freshness.** Arbitrum includes a parent-chain message after the parent finalizes it, by default. Measured: a root was readable on Robinhood **762 s** after it was sent, and the first render was **781 s** old; across Milestones 2–4 deliveries took **469–773 s**. Use `maxAgeSeconds` of at least 3600 on Robinhood. **Nobody delivers on a schedule.** Delivery is on demand — there is no keeper or operator — so between deliveries a proven value simply ages; proven values have been seen **10.6–10.8 hours** old (doc 179 §14.8). The render says so (`stale (…)` past your `maxAgeSeconds`); a paid or scheduled delivery is planned, not built (doc 181 §3.3). A relay root source (`RelayRootSource`, Grade C) is built but **not deployed**. ## Tokens ChainRender didn't create (snapshot mode) The proofs above read a token's balance **from its storage**, which works because a ChainRender token's storage layout is fixed and recorded by its factory. An existing token — WETH, USDC behind its upgradeable proxy, a rebasing token — has no such record, and nothing on-chain can check a claimed storage layout against a contract's code. So instead of reading its storage, Milestone 3 **runs its code**: 1. **Snapshot on Sepolia (anyone).** `BalanceSnapshotter` calls the token's own `balanceOf(holder)` and `decimals()` in a transaction, and stores a commitment to the answers — with the token's code hash, the block number and the block timestamp — in **its own storage**, whose layout is known. `recordMetadata(token)` does the same once for `name()` and `symbol()`. Ownerless, permissionless, no fee. 2. **Deliver a root** to the other chain, exactly as for Milestone 2. 3. **Prove and render.** `SnapshotBalanceCache` proves the snapshotter's commitment slots against the delivered root (the snapshotter's code hash is pinned), and `SnapshotBalanceResolver` (kind `xchain-snapshot-balance`) renders it. The token's code does the interpreting, under Sepolia's consensus, so the answer is a same-chain fact of the same kind ChainRender's view reads are: **Grade A**, relative to the host, with the host facet as before. Proxies, packed balance words and rebasing need no special handling — whatever `balanceOf` returned is what is shown. **What exactly it states.** "During a transaction in block N, this token's code (code hash H) answered `balanceOf(holder)` with X." That is a **mid-transaction** observation, which is why the render says `(in-transaction)`. It is meant for **display**: an app that gates access or value on a balance should use the end-of-block proofs above, or require two snapshots in different blocks that agree. | Situation | Render | |---|---| | a fresh snapshot | ` <0x full address> · balanceOf @ block N (in-transaction) · s old · Grade (on [ · ])` | | none fresh | `stale ( <0x full address> · balanceOf @ block N · Grade [ · on · ])` | | no metadata proven | `unavailable (no metadata)` | | metadata only under different token code | `unavailable (code changed)` | | no entry, bad arguments, out-of-bounds values | `unavailable` | The **full token address** is always shown: anyone can deploy a token that calls itself "USDC". Decimals come from the snapshot itself. Name and symbol are used only if they were recorded **under the same code hash** as the snapshot and **no later** than it, so a token replaced in place cannot borrow its predecessor's identity. The grade is the lowest of the snapshot's source, the metadata's source, and A. **What it does not establish** (published on-chain by the three contracts): the balance now; that the holder held it outside that call (mid-transaction state can be flash-borrowed); that `balanceOf` is honest (it is the token's own answer); that the token is what its symbol suggests; that its code is unchanged since (a proxy's code hash does not change on upgrade); that the issuer cannot change balances; and **that the snapshotter's fixed call gas was enough**. The snapshotter gives the token exactly `CALL_GAS` (100,000 here) and refuses to run with less, so a caller cannot starve it into a fallback answer; but a token that needs more and silently catches its own failure would answer wrong. Each token is therefore checked before use (`scripts/xchain/m3-gates.mjs`): its answers given `CALL_GAS` must equal its unbounded answers. Sepolia WETH9 and USDC pass; whole-transaction gas estimates for their calls were 17,670–25,923, far below the cap. **Freshness costs a Sepolia transaction.** A storage proof can read any later block for free; a snapshot is only as new as the last time someone paid for one. Past `maxAgeSeconds` the render says `stale (…)`. ```ts import { buildMetadataProof, buildSnapshotProof, encodeSnapshotBalanceArgs, KIND_XCHAIN_SNAPSHOT_BALANCE } from "./chainc/xchain-snapshot.js"; await snapshotter.write.recordMetadata([token]); // Sepolia, once per token (and after any code change) await snapshotter.write.snapshot([token, [holder]]); // Sepolia, each time a fresh value is wanted // … deliver a root for block n (as in Milestone 2), then on the host: const m = await buildMetadataProof(sepoliaClient, snapshotter.address, token, n); await cache.write.registerToken([source, n, token, m.name, m.symbol, m.codeHash, m.metaBlock, m.proof]); const s = await buildSnapshotProof(sepoliaClient, snapshotter.address, token, holder, n); await cache.write.refresh([source, n, token, holder, s.value, s.decimals, s.codeHash, s.snapBlock, s.snapTime, s.proof]); const line = await resolver.read.resolve([KIND_XCHAIN_SNAPSHOT_BALANCE, encodeSnapshotBalanceArgs(token, holder, 7200n, 2)]); ``` The builders take each commitment's preimage from the snapshotter's events and check it against the proven slot before anything is submitted. `scripts/xchain/m3-snapshot.ts` runs the whole sequence, and checks every on-chain render against the chainc mirror `renderSnapshotBalance` at the same block. Accepted on 2026-10-06 with Sepolia **WETH9** and **USDC**: the lines above, with the root on Robinhood after 773 s. ## Tokens ChainRender didn't create, without a transaction per value (storage-layout mode) Snapshot mode needs a Sepolia transaction for every fresh value. **Storage-layout mode** reads an existing token's storage directly, at any block a root source vouches for — but nothing on-chain can check that a slot really is where the token keeps its balances, so it is honest only at **Grade C**: ``` 0.03 WETH 0xfff9976782d46cc05630d1f6ebab18b2324d6b14 · proven @ block 11862735 · 488s old · Grade C (adopted · claim 0xfae8aac6…5d6cc4e5 · on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10) ``` **How a token is adopted.** Anyone files a **claim** with `TokenAdoptionRegistry` on Sepolia. A claim names two small **adapters** — a *pinning rule* ("which code is this token really running?") and a *balance layout* ("where is the balance, and how is it encoded?") — by address **and code hash**, plus the token's code hash, the keccak256 of a published **evidence bundle**, and a **witness** holder. At adoption the registry: - checks the adapters are **pure** — it scans their code and refuses any reachable opcode outside a short allowed list (no storage, calls, logs, block or transaction data), so an adapter can never change its answer over time; - runs **structural checks** on the token through the pinning rule — for `plain/v1`: no reachable `DELEGATECALL` or `CALLCODE` and a `balanceOf` selector; for `slot-proxy/v1`: the proxy reads one constant implementation slot and the implementation does not delegate further; - reads the token's name, symbol and decimals and the witness's `balanceOf`, under the same gas floor and return-size bounds as snapshot mode. In a later transaction anyone **confirms** the claim: a same-chain proof of the adoption block must show the claimed slot holding exactly the witness's balance. Only confirmed claims can be registered by a cache on any chain. **Claims are content-addressed.** The claim id is the hash of the whole record, so there is no "official" record per token: two people can adopt the same token under different claims, and a wrong claim only ever affects itself. An app binds a **specific claim id**, which the render prints (8+8 hex) next to the full token address. **What the code checks and what it does not.** The chain checks code hashes, structure and one witness. It does not check that the claimed layout is the token's real one — that rests on the **evidence bundle**: the token's verified source recompiled to its exact on-chain bytecode, the balance slot derived by the compiler, and at least three holders whose slot values equal their `balanceOf` at one block. `chainc/adoption-evidence.ts` reruns all of it; a client that does may show **Grade B** for that claim. On-chain, every adopted read stays **C**, whatever an adapter declares. **Upgrades stop proofs, never mislead them.** For a proxy, the claim pins the implementation slot's word and the implementation's code hash; after an upgrade every later proof reverts (`ImplementationChanged`) and the app must bind a new claim. Values proven before the upgrade still render as what they are — a past block, with its age. **Refused rather than mis-pinned.** Sepolia **USDC** is refused: its implementation delegate-calls a linked library (`Delegates` at instruction 18607). An on-chain scan cannot tell where a `DELEGATECALL` target comes from, so the rule refuses every reachable one rather than risk accepting a hidden upgrade path. USDC is shown through **snapshot mode** at Grade A instead. **Adding a kind of token = adding an adapter.** The registry, cache and resolver never interpret a layout; they only run the adapters a claim names. A new layout or proxy kind is one new adapter with its own review — the test suite proves it with a fourth, test-only adapter that works end to end with no change to the core. ```ts import { buildAdoptedStateProof, buildRegistryProof, encodeAdoptedBalanceArgs, KIND_XCHAIN_ADOPTED_BALANCE } from "./chainc/xchain-adopted.js"; // home: adopt (claim id + record from the Adopted event), then confirm the witness at the adoption block const { proof: w } = await buildAdoptedStateProof(sepoliaClient, record.claim, record.claim.witness, record.adoptedAt); await registry.write.confirmWitness([record, w]); // any chain with a root for block n: register the confirmed claim once, then refresh holders const { proof: rp } = await buildRegistryProof(sepoliaClient, registry.address, claimId, n); await cache.write.register([source, n, record, rp]); const { proof } = await buildAdoptedStateProof(sepoliaClient, record.claim, holder, n); await cache.write.refresh([source, n, claimId, holder, proof]); const line = await resolver.read.resolve([KIND_XCHAIN_ADOPTED_BALANCE, encodeAdoptedBalanceArgs(claimId, holder, 7200n, 2)]); ``` The proof builders ask the claim's own adapters (pure `eth_call`s) which accounts and slots to prove, exactly as the contracts do, so a new adapter needs no new client code. `scripts/xchain/m3b-adopt-and-prove.ts` runs the whole sequence and checks each render against the chainc mirror. Accepted on 2026-10-07 with Sepolia **WETH9** — its verified source recompiles byte for byte; balance slot 3 — rendered on Sepolia and, 469 s after delivery, on Robinhood. **Which mode to use.** Snapshot mode: Grade A, any ERC-20 including USDC, one Sepolia transaction per fresh value. Storage-layout mode: Grade C, no transaction per value, any later block, only for tokens whose code passes the structural checks. ## Cost Measured on Sepolia: a `refresh` with a real 9-node account proof costs **561,811 gas** the first time for a holder (creating its storage) and **510,415 gas** when updating it, the latter measured after Glamsterdam (2026-10-06), which prices newly created storage higher; registering a token is a one-time cost, measured at roughly 430,000–480,000 gas in local tests (where proofs are shallower than on Sepolia, so expect more there). Rendering is free. Proof depth grows with the size of the chain's state, adding roughly 30,000 gas per extra proof node. On Robinhood (measured 2026-10-06): delivering a root cost **219,748 gas** on Sepolia plus about 0.0000122 ETH of prepaid ticket fees (estimates × 3; the unused part is refunded on Robinhood). The delivered root's execution on Robinhood cost 69,051 gas. Registering CRPD reported **881,376 gas** and a refresh **813,008 gas**, but an Arbitrum receipt includes the parent-chain data fee for the calldata (about 28–30 % here). The execution part of the refresh, **567,862 gas**, is within about 1 % of the same first refresh on Sepolia (561,811). At Robinhood's 0.01 gwei, a register plus a refresh cost about 0.000017 ETH. Snapshot mode (measured 2026-10-06, first time for each token and holder): on Sepolia, `recordMetadata` cost **151,815** (WETH9) and **155,432** gas (USDC), and `snapshot` for one holder **141,610** and **149,825** gas. On Robinhood the execution was **589,189 / 586,969** gas to register the metadata and **543,070 / 543,058** to refresh — below Milestone 2's 633,099 and 567,862, because the proof ends in the snapshotter's small storage (2 storage-proof nodes) rather than a token's. Storage-layout mode (measured 2026-10-07, WETH9, first time for each piece of state): on Sepolia `adopt` **1,393,910** gas (copies and scans the token's code; scans both adapters the first time they are used) and `confirmWitness` **650,042**; registering the claim in a cache **2,485,171** on Sepolia and **907,728** on Robinhood (the full adoption record is stored); a first refresh **881,362** on Sepolia and **711,951** on Robinhood. WETH9's storage trie is deep (6 storage-proof nodes against the snapshotter's 2), which is why a refresh costs more than in snapshot mode. New state cost about 2.7× more on post-Glamsterdam Sepolia than on Robinhood in the same run. **Once set up, a fresh value costs less** (repeat refreshes, measured 2026-10-07, all entries already existing): | | Sepolia | Robinhood | |---|---|---| | Snapshot mode | `snapshot` **43,690** (WETH9) / **51,905** (USDC) | `refresh` **486,077** / **486,053** | | Storage-layout mode | `refresh` **675,618** (same-chain) | `refresh` **674,987** | Plus, for Robinhood, one root delivery per block: **219,712** Sepolia gas and a prepaid ticket. One delivery serves every refresh at that block, for any number of tokens, holders and both modes. The two chains charged almost exactly the same for the same repeat refresh, so the earlier gap came from creating new state, not from verifying proofs. ## Security review The proof stack went through two independent review passes before deployment. Every finding was fixed and has a test, including: per-source entries so a relay can't lock a value, time-based staleness so a silent relay can't keep its values fresh, display bounds on symbols and decimals, entries that stop rendering when the record they were proven under is superseded (tested against a fully forged relay state), strict trie rules, and a scope-disclosure reader that a hostile contract can't make revert. The details are in the implementation spec (`docs/176-xchain-milestone1-implementation-spec.md`, §10.4). Milestone 2's spec, gate results and acceptance run are in `docs/177-xchain-milestone2-robinhood-spec.md` (§10–§12); Milestone 3's design reviews, as-built notes and acceptance run are in `docs/178-xchain-milestone3-adopted-tokens-spec.md` (§12–§16). ## Swapping across chains without a bridge (Milestone 4) Proven balances never move value. Milestone 4 adds the one way to exchange value across Sepolia and Robinhood that needs no bridge and no escrow anyone else controls: a **hash-timelock swap**. Alice locks a Sepolia token under the sha256 hash of a secret; Bob locks a Robinhood token under the same hash; revealing the secret on Robinhood pays Alice, and the same secret then pays Bob on Sepolia. If nobody claims, each lock refunds its owner after its own deadline. The token on each chain never leaves that chain; only its owner changes. What ChainRender adds is that **the second lock checks the first one by storage proof.** Bob's Robinhood lock names `SwapLockProofCache` as its guard and opens only if Robinhood has proven that Alice's Sepolia lock is open, locks a proven ChainRender token, uses the same hash, pays Bob, and outlasts Bob's lock by a safety gap. The gap and Bob's minimum lock length are derived from Robinhood's own time bounds (its SequencerInbox's `maxTimeVariation`), so Alice can still claim through Sepolia's delayed inbox if Robinhood's sequencer ignores her. A `swap-lock` resolver renders the proven lock: ``` Sepolia lock open · 25.00 CRSA · to 0x256f87f6…59c180ac · ends in 305h 40m · proven @ block 11864092 · 612s old · Grade A (on Robinhood Testnet · instant upgrades, 1-of-2 Safe, as of 2026-10) ``` The proven state is Grade A relative to Robinhood, with its facet, like every proven read there. **The swap as a whole is Grade C at most**: it needs each claim included before its deadline, and Robinhood's operators can delay transactions. The same `HashTimelockSwap` contract, at the same address with the same code, runs on both chains. The showcase page (`/chainrender-crosschain.html` on the site) walks the whole use case with the visitor's own wallet: create a token on either chain (name, symbol, decimals, supply), send on each chain, prove a balance across chains, swap, follow every event per chain, and compare the trust model with Robinhood's canonical bridge, tacit and EEZ. Status: live on Sepolia and Robinhood Chain Testnet since 2026-10-07, accepted end to end (addresses in [Deployed Contracts](../contracts/deployed-contracts.md)). In the acceptance run a Sepolia root took 591 s to reach Robinhood; proving the lock there cost 651,525 gas, the guarded second lock 159,466, and each claim 71,967 (Robinhood) and 168,495 (Sepolia). On this testnet Robinhood allows a 4-day delay window, so the second lock must run at least 8.5 days and the first about 4.1 days longer: a swap where both sides claim settles in minutes, but a refund waits that long. Design, timing rules, review and the as-run record: `docs/179-xchain-milestone4-transfers-spec.md`. ## What's next - **More adapters.** Beacon proxies, minimal (EIP-1167) proxies, other packings and multi-slot layouts (vault shares, rebasing tokens) — each one new adapter, no change to the registry, caches or resolver. - **Proven view calls without a transaction.** A zero-knowledge proof of a `balanceOf` call against a block's state would give snapshot mode's Grade-A answer without a Sepolia transaction per value; it plugs into the same seams. - **Spending stays home.** Proven balances are display and gating only. Value changes hands across chains only by the swap above, where each token stays on its own chain. A spendable copy of a token on another chain would need a bridge, and on an instantly upgradeable host that is Grade C at most (the upgrade-key rule). - **Proofs in the other direction.** Robinhood-home tokens can't yet be proven on Sepolia; that needs a root source reading Robinhood's confirmed state from its rollup contract on Sepolia. ## Related - [All Resolver Kinds](../reference/resolver-kinds.md) — exact args and renders of `xchain-balance`, `xchain-snapshot-balance`, `xchain-adopted-balance` and `swap-lock`, generated from the code - [The Trust Grade](./trust-grade.md) — the A / B / C model these grades follow - [Resolvers](./resolvers.md) — how a data slot binds a resolver kind - [DeFi Capabilities](./defi-capabilities.md) — per-chain DeFi reads - [Deployed Contracts](../contracts/deployed-contracts.md) — the live addresses --- # Intent DeFi & CoW Protocol Most DeFi is **transactional**: you sign a specific swap at a specific block. **Intent-based** DeFi is different — you express *what you want* ("sell 10 WETH for USDC over the next day, never below this limit") and a network of solvers competes to fill it. [CoW Protocol](https://docs.cow.fi) is the canonical intent layer: its **ComposableCoW** framework lets a single signed order describe a *programmatic* or *conditional* order — a TWAP, a stop-loss, a good-till-cancelled limit — that becomes tradeable only when its on-chain conditions hold. Intent orders are exactly the kind of thing users sign without being able to read. ChainRender's job here is the one it always has: **render the on-chain truth of the intent, and verify what the user is about to sign — trustlessly, as a free `view` — and never pretend to be the solver, the settlement layer, or CoW's order book.** Three resolvers cover the lifecycle, each ownerless, immutable, view-only, and fail-closed. ## The mental model: render + verify the intent, never *become* the auction CoW settlement happens in an **off-chain batch auction**: solvers find coincidences of wants, and the winning settlement is submitted to CoW's settlement contract. That auction is not something any `eth_call` can see, and ChainRender does not fake it. What *is* on-chain and readable: 1. **The order definition** — a TWAP/conditional order's parameters (schedule, limits) are deterministic data. Rendering the schedule is pure arithmetic over that data + `block.timestamp`. 2. **Tradeability this block** — ComposableCoW itself exposes a `view` that returns the discrete order a conditional order *would* emit right now (or reverts if none is valid yet). That is a trustless read of "is this live?". 3. **App-data integrity** — a CoW order commits to a `keccak256` of its full app-data document (metadata + pre/post hooks). Recomputing that hash on-chain proves the human-readable document you are shown is the exact one the order bound. What is **not** a floor fact, and is never rendered as one: whether a batch will actually settle, at what price, and when — that depends on solver competition in the off-chain auction. A "ready/settled" badge would be a *labelled client enhancement* over CoW's Order Book API, not part of this on-chain floor. ## `cowOrder` — live tradeability (the real ComposableCoW) `CowConditionalOrderResolver` **STATICCALLs the real ComposableCoW** — `getTradeableOrderWithSignature(owner, (handler, salt, staticInput), offchainInput, proof)` — and renders the discrete `GPv2Order` the conditional order would emit **this block**. When no order is valid yet (the normal "not yet executable" state), the call reverts and the resolver **fails closed to `"not active"`** — never a fabricated order. | kind | renders | |---|---| | `cowOrder` | a field of the tradeable `GPv2Order` this block — sell amount, buy amount, `validTo`, or partially-fillable — or `"not active"` | Args are caller-supplied: `abi.encode(composableCow, owner, receiver, salt, staticInput, offchainInput, proof[], sellDecimals, buyDecimals, field)`. Because the result is a `view` recomputable by anyone against the real ComposableCoW, an honest tradeability read grades **A (Trustless)**. The resolver renders what ComposableCoW returns — it does **not** assert the order is registered, nor that a batch will settle it; it answers exactly one question: *would this order be tradeable right now?* > ComposableCoW is deployed deterministically at the same address on every supported chain: > `0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74`. The resolver takes it as an argument — ChainRender hardcodes nothing. ## `cowTwap` — the schedule (pure, trustless arithmetic) `CowTwapResolver` decodes a **TWAP order definition** (`TWAP.Data`: part sell amount, minimum part limit, start `t0`, number of parts `n`, interval `t`, and span) and projects it against `block.timestamp` to render the human schedule: which part is active, the countdown to the next slice, the per-part sell size, and the per-part limit. | kind | renders | |---|---| | `cowTwap` | part `X / N`, next-slice countdown, per-part sell amount, per-part minimum limit, or a status label | Args: `abi.encode(twapData, sellDecimals, buyDecimals, field)`. This is **pure arithmetic over the definition plus the block clock** — nothing but `block.timestamp` is read, so it is fully recomputable via `eth_call` and grades **A**. The honest caveat is stated on every card: `cowTwap` renders the schedule the *definition* encodes; it is **not** proof the order was registered with ComposableCoW. Pair it with `cowOrder` (which *does* read the real ComposableCoW) for the full picture — the schedule story next to live tradeability. ## `cowIntent` — "verify what you sign" (on-chain integrity + a client decoder) This is the user-protection piece. A CoW order commits to `order.appData == keccak256(fullAppData)` (per CoW's app-data SDK). `CowIntentVerifyResolver` recomputes that hash **on chain** and compares it to the hash the order bound: | kind | renders | |---|---| | `cowIntent` (field 0) | `"verified"` if `keccak256(fullAppData) == expected`, else `"app-data mismatch"` | | `cowIntent` (field 1) | the computed `keccak256` of the presented document, as hex | Args: `abi.encode(fullAppData, expected, field)`. Flip **one byte** of the document and `"verified"` becomes `"app-data mismatch"`. Crucially, the verdict is an **integrity** statement, never a safety one — it proves the document you are shown is byte-identical to the one the order committed to; it **never says "safe to sign."** Grade **A**, fail closed. ### The ⚙ client decoder (`cr-cow-intent.js`) A CoW app-data document carries the atomic plan — **pre/post CoW Hooks**, each `{target, callData, gasLimit}` — that will run around the swap. On-chain JSON parsing is impractical, so decoding the plan into plain English is a **client enhancement (Grade ⚙)**, not a floor read. `cr-cow-intent.js` does exactly that, under strict rules that keep it honest: - It **only runs after** the on-chain `cowIntent` slot returned `"verified"`. If the hash did not match, it refuses to decode — the document is not the one the order committed to. - It decodes **known selectors** (`approve`, `transfer`, `transferFrom`, `permit`, `withdraw`, `stake`, …) into a readable step; **any unknown selector is marked `OPAQUE`** and surfaced, never guessed. - It **fails closed** and **never emits "safe to sign."** The worst case is an honest "this step could not be decoded — verify it manually." This is the **msg-712 / sig-1271 split** in miniature: the trustless floor proves *integrity* (the bytes are the committed bytes), and a clearly-labelled client layer adds *legibility* (what those bytes do) over the already-verified document. The enhancement can only ever make a verified document more readable — it can never upgrade an unverified one. ## Putting it together — the trustless order monitor The Explorer ships two CoW example cards built entirely from these reads: - **Trustless CoW order monitor** — `cowTwap` renders the schedule (live via `block.timestamp`) beside `cowOrder`, which STATICCALLs the real ComposableCoW for live tradeability, failing closed to `"not active"`. The schedule story next to the live-tradeability truth, with zero client trust. - **Verify what you sign** — `cowIntent` shows a matching document rendering `"verified"` and a tampered hash rendering `"app-data mismatch"` side by side; the hook plan is decoded client-side (`cr-cow-intent.js`) only after the on-chain verdict. Both are free `eth_call` renders — no server, no CoW API key, no indexer. ## The honest boundaries (read these) - **Settlement is off-chain and is not rendered as a floor fact.** CoW fills orders in an off-chain batch auction; whether/when/at-what-price a batch settles depends on solver competition. ChainRender renders *tradeability* and the *schedule* (both on-chain), not a settlement guarantee. A "settled/ready" badge would be a labelled client layer over CoW's Order Book API. - **`cowTwap` renders the definition, not proof of registration.** It is the schedule the parameters encode; use `cowOrder` against the real ComposableCoW to confirm the order is actually live. - **`cowIntent` proves integrity, not safety.** A `"verified"` document is the one the order bound — it is **not** a judgment that signing is wise. The decoder reinforces this: unknown calls are `OPAQUE`, and it never says "safe." - **ChainRender blesses no order or handler.** Every ComposableCoW address, owner, handler, and app-data document is a caller argument; nothing is hardcoded or endorsed. ## What this is not ChainRender is **not a CoW solver**, does not run or replace CoW's **off-chain batch auction** or **Order Book API**, does not custody funds, and does not settle trades. It renders the on-chain truth of an intent order and verifies the document behind it — the trustless half — and is deliberately silent on the off-chain auction it cannot see. ## Deployed (Sepolia) CoW intent-DeFi resolvers are deployed on **Ethereum Sepolia** (where ComposableCoW lives); they are portable to any chain where ComposableCoW is deployed, since the ComposableCoW address is a caller argument. Verify against `deployment/additive/addresses.json` before integrating. | Resolver | Kind(s) | Sepolia address | Grade | |---|---|---|---| | **CowConditionalOrderResolver** | `cowOrder` | `0x752AcEd4a7D7bD9Bd604c3d7adD38373Eed3b6c1` | A | | **CowTwapResolver** | `cowTwap` | `0xF949E6fB52Df34Dcc86F674E51327567cD964395` | A | | **CowIntentVerifyResolver** | `cowIntent` | `0xCaa6E85510f6FDDA54Be92AaEb95e6779bdc1AB2` | A | | ComposableCoW (CoW Protocol, external) | — | `0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74` | — | ## Where to go next - **[DeFi Capabilities](./defi-capabilities.md)** — the full render-and-declare DeFi model this builds on. - **[Actions & Signing](./actions.md)** — the declared-action decoder; `cowIntent` is its intent-order analogue. - **[The Floor](./the-floor.md)** — why integrity is the floor and the hook decode is a ⚙ enhancement. - **[Trust Grade](./trust-grade.md)** — why these on-chain reads grade A. - **Reference: [Resolver Catalog](../reference/resolvers.md)** — exact kinds + arg shapes · **[Integrations](../reference/integrations.md)**. --- # RWA capabilities ChainRender is a rendering + on-chain-**verification** layer, not an asset platform: it issues nothing and custodies nothing. Its floor guarantee is a faithful render of on-chain state — never a guarantee that the underlying real-world fact is true. For real-world assets that makes it strong at **displaying and cryptographically verifying** asset state, and deliberately silent on custody, settlement, and legal wrappers. Every resolver below is deployed on all three testnets (Sepolia, Robinhood, Circle Arc), ownerless and immutable, view-only, and fails closed — a reverting or missing source renders `"unavailable"`, never a fabricated value. ## Proof of Reserve `ProofOfReserveResolver` reads a reserve source (an attested reserves figure and/or a Chainlink Proof-of-Reserve feed) and a token's `totalSupply()`, and renders reserves, supply, and a **coverage ratio**, with freshness enforced (a stale feed renders `STALE`, not a number). | kind | renders | |---|---| | `por-reserves` | attested/oracle reserves, decimal-formatted | | `por-supply` | the token's on-chain `totalSupply()` | | `por-coverage` | reserves ÷ supply as a coverage percentage | Args are caller-supplied — `abi.encode(feed, token, fracDigits, maxStaleSeconds)` — so the resolver has no embedded dependency and the same deployment reads any feed/token the caller points at. Because the coverage ratio is **computed on-chain from view calls**, an honest reserve display grades **A**: an under-collateralised position renders as exactly that (e.g. 95%), never hidden. ## ERC-3643 permission display `Erc3643PermissionAdapter` reads an ERC-3643 permissioned-token stack — its identity registry and compliance module — and renders holder/compliance state. It is **display, not enforcement**: it shows whether a transfer *would* be permitted; it never gates one. Anything that moves value or grants a role must re-check on-chain (a view call's result can be computed against any address a caller claims). | kind | renders | |---|---| | `erc3643-verified` | whether a holder is identity-verified | | `erc3643-frozen` | whether a holder / amount is frozen | | `erc3643-status` | a combined compliance-status label | | `erc3643-can-transfer` | whether a proposed transfer passes compliance | Its reads are genuine on-chain view calls, so the *data edge* is on-chain — but because the identity registry and compliance module are **mutable, authority-controlled dependencies** (an admin can change verification or freeze state at any block), the capability is graded **≤ C** on the dependency axis. That is the honest trust envelope: the render is a faithful snapshot of a mutable authority's current state, not a trustless guarantee. ## Credential lifecycle (issuer-binding, expiry, revocation) `CredentialStatusRegistry` is an ownerless, issuer-scoped registry where an issuer can attest, renew, and revoke a credential, each with an expiry; `CredentialLifecycleResolver` renders its state. This closes the gap left by a bare "member-of-set" proof: a credential now has a real KYC/accreditation lifecycle. | kind | renders | |---|---| | `cred-status` | `UNKNOWN` / `VALID` / `EXPIRED` / `REVOKED` | | `cred-valid` | a single boolean, bound to an expected issuer | Status is derived from an on-chain read (data edge on-chain); the expected-issuer binding means a credential attested by the wrong key reads as `UNKNOWN` rather than being silently trusted. As with ERC-3643, the registry is a mutable authority, so a credential badge is a faithful render of the issuer's current attestation — not proof the underlying claim is true. ## Tokenized-vault NAV (ERC-4626 / ERC-7540) `VaultResolver` reads a tokenized vault — the standard shape for tokenized treasuries, money-market funds, and other yield-bearing RWA wrappers — and renders its share price and queue state trustlessly. It covers both the synchronous **ERC-4626** surface and the asynchronous **ERC-7540** redemption/deposit queue in one resolver. | kind | renders | |---|---| | `vault4626` | NAV per share (`convertToAssets` of one share), total assets, or share supply | | `vault7540` | the async ERC-7540 queue — pending / claimable redeem shares, pending / claimable deposit assets | Args are caller-supplied — `vault4626` takes `abi.encode(vault, assetDecimals, shareDecimals, field)`; `vault7540` takes `abi.encode(vault, controller, requestId, assetDecimals, shareDecimals, field)` — so the same deployment reads any 4626/7540 vault the caller points at. Both read genuine on-chain `view`s and grade **A** on the data edge. The **trust boundary is the vault's own accounting**: `convertToAssets` / `totalAssets` are what the vault *reports*, not a claim that its valuation is correct — the card states this, exactly like the Proof-of-Reserve and oracle cards. It fails closed to `"unavailable"` on a non-vault, a revert, or degenerate decimals (`_pow10` guards decimals > 36). The Explorer demonstrates it over an **immutable `DemoVault`** (no setters) so the NAV and queue render live; re-point the slot args at a real ERC-4626/7540 vault with no code change. This composes with `por-coverage` (backing) and `price` (NAV in USD) for a full tokenized-vault panel, and closes the top RWA gap beyond proof-of-reserve. ## What this is not There is deliberately no chain-level compliance **enforcement** (no deployed identity registry / partitions / freeze / forced-transfer of our own), no custody, no settlement or on-ramp, and no legal-wrapper enforcement. Render-time gates are UX, not a security boundary. The genuine RWA differentiator here is the honest floor: on-chain reads rendered faithfully, with the trust grade telling you exactly how much to trust each one. ## On the roadmap — watch list - **ERC-8262 (Zero-Knowledge Compliance Oracle, Draft).** A standard for on-chain ZK compliance attestations (AML/sanctions) via UltraHonk proofs. Not implemented. If a live ERC-8262 oracle appears, the in-scope fit is a **display-only** compliance-status resolver in exactly the ERC-3643 mould above — render the oracle's boolean/ attestation, never be the oracle. Its UltraHonk proving system is the Barretenberg/Noir stack already chosen for the credential/privacy surface, so the toolchain aligns if the prover side is ever built. --- # Reputation on ChainRender — Complete Capability Guide ChainRender can build **fully on-chain reputation UI**: apps that render an address's social graph (via EFP), delegated vouches and flags, credential and attestation status, and identity claims — as `eth_call` renders, honestly graded. This is the comprehensive reference for what ChainRender can and cannot do with reputation, and the honest boundaries. ## The mental model: read + render reputation, never *become* the authority ChainRender does two things for reputation, and deliberately not a third: 1. **Render on-chain reputation** — follows/tags/blocks, vouches, credential status, attestations, identity claims — as free `view` calls. No server, no indexer for the trustless parts; the data is read from the source's own contracts at render time. 2. **Declare reputation actions** — a button can carry a declared action to *write* a vouch or follow (governed by the capability envelope + action decoder), just like any other action. 3. **It does NOT become a reputation oracle.** ChainRender never mints a score, never ranks addresses, never blesses an attester, and never runs an owned identity registry. A reputation authority with an admin key is exactly the thing the platform exists to avoid. Reputation is **read and rendered**, never manufactured. This is the same separation as [DeFi](./defi-capabilities.md) (render prices, don't become a DEX) and [RWA](./rwa-capabilities.md) (display compliance, don't become the compliance authority). ## What ChainRender can RENDER (the reputation surface) Each resolver is immutable, ownerless, **view-only, fail-closed**: a reverting/missing source renders `"unavailable"` rather than breaking the page. Addresses are always **caller-supplied** — ChainRender blesses no list, attester, or issuer. ### EFP — the on-chain social graph (Ethereum Follow Protocol) `EfpResolver` reads EFP list ops on chain and replays them to answer a **forward** question about a caller-named list. | Kind | Renders | |---|---| | `efp-follows` | Does this list currently follow the target? | | `efp-tag` | Does this list tag the target with a given tag (`friend`, `phisher`, …)? | | `efp-state` | Compact: `following` / `blocked` / `muted` / `not following` | | `efp-list-size` | The list's raw op count (a list-size figure, **not** a follower count) | **Forward reads are Grade A** — a real on-chain read, recomputable by anyone via `eth_call`, bounded to a scan cap (a larger list renders `"unavailable"`, never a partial). **Follower counts and reverse lookups ("who follows X") are deliberately not offered on chain** — they need a global reverse index no single contract holds. Those belong to the EFP indexer/API and must be graded **off-chain (≤ C)** and labeled — never surfaced as an on-chain fact. ### Delegated address vouching (native, ownerless) `AddressAttestationRegistry` is an ownerless registry where any attester vouches for (or flags) an address under a caller-chosen topic, with expiry and revocation — `msg.sender` is the attester; no owner, no admin. `AddressReputationResolver` renders it. | Kind | Renders | |---|---| | `rep-vouch` | One attester's status for a subject+topic: `vouched` / `expired` / `revoked` / `unknown` | | `rep-tally` | A transparent **k-of-n** over an attester set the *caller* supplies | Each vouch *record* is an on-chain fact (`READ_CHAIN`), but its *meaning* is delegated to a mutable attester the consumer chose — so a reputation signal from it grades **≤ C** (the dependency axis, the same as [ERC-3643](./rwa-capabilities.md)). **There is no global score:** `rep-tally` is "k/n" over *your* chosen attesters — change the set, change the answer. A topic is neutral to sign (`trusted-builder` or `scammer` both render). ### Cross-source aggregate `ReputationAggregateResolver` (`rep-aggregate`) composes many sources — EFP, vouches, EAS, ERC-725/735, or a future source — through a uniform adapter (`IReputationSource`) into a transparent **k-of-n tally whose grade is capped at the weakest source**. An A-grade EFP follow can never launder a C-grade vouch into trustlessness. Still no opaque number. ### Attestations & credentials (existing) - **`AttestationAdapter`** reads **EAS**: "subject X holds attestation Y from attester Z". See [Data Sources](../reference/data-sources.md). - **`CredentialStatusRegistry` + `CredentialLifecycleResolver`** — issuer-bound KYC/accreditation lifecycle (`VALID / EXPIRED / REVOKED / UNKNOWN`). See [RWA Capabilities](./rwa-capabilities.md). - **ZK selective-disclosure credentials** (`CredentialResolver`, member-of-set, bound single-use). See [Privacy & Credentials](./privacy.md). ### Identity naming (existing) - **ENS** — `EnsProfileResolver` (forward-verified reverse resolution), records, ENS-v2/CCIP. The anchor most reputation UIs hang off. See [ENS Names & Integration](../guides/ens-integration.md). ## ERC-725Y / ERC-735 accounts ERC-725Y is a generic on-chain key-value store; ERC-735 layers signed claims on top. It's the **standard behind LUKSO Universal Profiles**, but it is not LUKSO-specific — any EVM chain can host an ERC-725Y/735 account. ChainRender's stance is the same as everywhere: **read it, don't become it.** - **Read today (same-chain):** an ERC-725Y data key or an ERC-735 claim is a `view` read — render it now with the generic [`staticread`](./rendering-uniswap-hooks.md) resolver (`CallResolver`), pointed at the account address and data key. `CallResolver` does a `STATICCALL` against a **co-located** contract (the chain the resolver is deployed on), so it reads ERC-725Y/735 accounts that exist **on that same chain** — Sepolia, Robinhood, or Arc today. - **LUKSO is not integrated as of now.** LUKSO Universal Profiles live on the LUKSO chain (mainnet 42 / testnet 4201). An EVM `STATICCALL` is same-chain only, so a resolver on Sepolia/Robinhood/Arc **cannot** read a UP that exists only on LUKSO — it fails closed to "unavailable." Reading live LUKSO UPs would require ChainRender **deployed on LUKSO** (a clean additive future step, since LUKSO is a standard EVM chain), or an off-chain (≤ C) fetch. Neither ships today. - **Never deploy our own:** an ERC-725 account is **owner-controlled** by design — deploying one would reinstall the admin key the platform refuses. The ownerless `AddressAttestationRegistry` is the on-brand equivalent for vouches. - **Grade:** an ERC-725Y store / ERC-735 claim is an owned, mutable authority, so a rendered claim grades **≤ C** (a faithful render of the owner's current data, not proof it's true) — exactly the ERC-3643 precedent. - **Planned:** a dedicated `Erc725ClaimResolver` (a thin, read-only reader with LSP2 key vocabulary) is a natural additive follow-up; until then `staticread` covers same-chain reads. ## The honest boundaries (read these) - **Follower counts are off-chain.** The trustless EFP reads are the *forward* relationship and personalized bounded reads; global counts come from the indexer and must be graded ≤ C and labeled. A follower count never renders Grade A. - **Reputation is not truth.** Every non-EFP-record signal rests on a mutable author (an attester, an issuer, an ERC-725 owner). A rendered badge is "the current claim of authority Y," never "this is true." Signals fail closed to `unknown`/`unavailable`, never a fabricated positive. - **No global score.** Any aggregate is caller-defined, transparent, and capped by its weakest input. A hidden reputation number is off-limits on principle — marketing must never exceed what the code enforces. - **The source must exist on the target chain.** EFP lives on Ethereum L1 / Base / Optimism; a resolver reads EFP where EFP *is* (the caller supplies chain + list), consistent with the caller-supplied-address convention. ## What's NOT built (honest gaps) - **`Erc725ClaimResolver`** — read ERC-725Y/735 claims with a typed LSP2 vocabulary (today: `staticread`). - **On-chain follower aggregation** — not possible trustlessly; the EFP indexer path is the honest route (≤ C). - **A weighted aggregate** — the shipped aggregate is unit k-of-n by design (transparent); weighting would be a later, still-caller-visible variant. ## Trust grade for reputation apps | Signal | Grade | Why | |---|---|---| | EFP forward read (`efp-*`) | **A** | real on-chain state, recomputable via `eth_call` | | EFP follower count / reverse lookup | **≤ C** | off-chain indexer aggregation | | Vouch / `rep-tally` / `rep-aggregate` | **≤ C** | delegated to a mutable attester the consumer chose | | EAS attestation / ERC-725 claim read | **≤ C** | mutable authority | | Aggregate score | **= weakest input** | never exceeds its inputs | ## Built to evolve (immutability-safe) Reputation is a moving space — EFP and ERC-725 will keep shipping capabilities. The suite is **additive by design**: a new or upgraded source plugs in as a new `IReputationSource` adapter address (the aggregate needs no change); a new EFP encoding ships as a new resolver (the shared `EfpOps` decoder is the single place it changes); new vouch semantics are just new topics (the registry's `bytes32 topic` is opaque). Deployed code is never edited — it is superseded additively. Full detail in the internal doc `149-reputation-resolvers.md` (§"Extension & versioning"). ## Where to go next - **[Resolvers](./resolvers.md)** — how the resolver mechanism works in general. - **[RWA Capabilities](./rwa-capabilities.md)** — credential lifecycle, ERC-3643, Proof-of-Reserve. - **[DeFi Capabilities](./defi-capabilities.md)** — the sibling render+declare guide. - **[Trust Signals & Lineage](./trust-signals.md)** — attestations (vouch) + advisories (alert) for components. - **[Privacy & Credentials](./privacy.md)** — ZK selective-disclosure credentials. - **Reference: [Integrations](../reference/integrations.md)** — every integration + library + SDK/skill/MCP at a glance. - **Reference: [Resolver Catalog](../reference/resolvers.md)** — every resolver + kind + args. --- # The Trust Grade Every ChainRender app carries a **trust grade** — a single letter (A / B / C) that tells a user, *before they interact*, exactly how trustless the app is. It is computed on-chain, and the SDK computes an identical grade locally (they are drift-locked), so you can see your app's grade as you build it. ## What the grades mean | Grade | Word | Meaning | |---|---|---| | **A** | Trustless | Fully on-chain, no external dependencies. Everything you see is computed from chain state. | | **B** | Pinned | Depends on an immutable, content-pinned external asset (e.g. an IPFS hash). Verifiable, but off-chain. | | **C** | Live | Depends on something that can change — a live off-chain fetch, a mutable pointer, or an unproven asset. | The grade is the **worst link**: one Grade-C part sets the headline, because a chain is only as trustless as its weakest dependency. But the grade is never just a letter — it comes with an **itemized breakdown** naming *every* deviation, so the user (and you) can see exactly *why* a grade was assigned. ## Why a grade instead of a checkmark A binary "safe / unsafe" badge would be dishonest — trust is a spectrum, not a boolean. An app that renders a chart from live on-chain data (Grade A) and an app that embeds an image from a random web server (Grade C) are *both* "functional," but they are not equally trustworthy, and pretending otherwise would violate ChainRender's core discipline: **marketing must never exceed what the code enforces.** The grade makes the trust cost *legible*. A user who wants maximum assurance uses only Grade-A apps. A user who accepts a pinned dependency uses Grade-B. Nobody is surprised, because the grade — and its reasons — are visible up front. ## How it's computed (the reasoning) The grade derives from the app's **capability envelope** — the union of everything the app can do and everything it depends on. The enforcer walks the composition and, for each part, classifies its dependencies along several axes (is data on-chain or fetched? is an asset immutable, attested, or mutable? does an action stay within the declared envelope?). Each axis has an honest floor: - **On-chain computation** → contributes nothing negative (Grade A). - **A fail-closed enhancement** (e.g. a GPU effect that degrades to an on-chain poster, or a fetch that falls back to complete on-chain data) → *stays* Grade A, because the trustless floor is intact even if the enhancement fails. - **A pinned/attested dependency** (a raw-CIDv1 IPFS asset, pointer anchored on-chain, client-verified, fail-closed) → Grade B. See the guide *[Grade B — a Pinned-IPFS Dependency](../guides/pinned-ipfs-dependency.md)* for how to declare it (`grade(app, { pinnedDep })`). - **A live fetch or mutable pointer that the output actually depends on** → Grade C. Crucially, the enforcer is **conservative and fails closed**: anything it cannot analyze is treated as unsafe (refused or downgraded), never optimistically passed. An unknown vocabulary token is refused, not rendered. ## The relationship to safety-by-construction The grade *describes* trust; the **enforcer** *enforces* safety. They're related but distinct: - Some things are **refused outright** — an arbitrary-code escape hatch, an action that moves value while the app never declared it can, a `{to, selector}` not in the declared call set. These don't get a bad grade; they're *rejected at registration*. See **[The Capability Enforcer](./enforcer.md)**. - Everything that *passes* gets an honest grade reflecting its real dependencies. So a registered ChainRender app is always *safe* (it can't smuggle a hidden transaction), and its *grade* tells you how *trustless* it is on top of that baseline safety. ## Using the grade as a developer ```ts import { grade } from "@chainrender/client"; const g = grade(myApp); console.log(g.letter); // "A" console.log(g.word); // "Trustless" console.log(g.deviations); // itemized: one { part, axis, reason, grade } per axis below A, if any console.log(g.aboveFloor); // set when something is filled above the floor: { kinds, word: "Above-floor", locks: "⚙" } ``` An app with anything filled above its floor is shown as its floor's letter **and** ⚙ Above-floor — never the letter alone. `aboveFloor.kinds` says what: `p5` / `three` (a sketch that runs client-side, see **[Rich Media: p5.js & three.js](./rich-media-p5-three.md)**), `viewer-read` and `transfer-history` (the viewer's own wallet data and transfer lists, read in the browser after the person opts in, see **[Viewer Reads](../guides/viewer-reads.md)**). `aboveFloorText(kinds)` gives the wording every surface uses. Watch the grade as you build. If adding a feature drops you from A to C, the breakdown tells you exactly which dependency did it — and you can decide whether that trade-off is worth it, *before* your users ever see it. ## Resolvers and the grade (an important distinction) Not all resolvers are equal for the grade: - A **client-side resolver** — a JS function that fetches with viem/ethers in the browser (e.g. `getBalance`, `getBlock`) — is an off-chain fetch. It grades **C (Live)**. Routing data through a resolver function does not make it on-chain. - An **on-chain resolver contract** — a resolver *address* referenced by a Slot — reads and renders the value on-chain during the `eth_call`. It grades **A (Trustless)**. So a Slot bound to an on-chain resolver contract is Grade A; a Slot filled by a client-side JS resolver is Grade C. The grade tells the truth either way — an app using client-side resolvers is not "fully on-chain," and the grade correctly reflects that. ## Related - **[The Capability Enforcer](./enforcer.md)** — what gets refused vs. graded. - **[Content Addressing](./content-addressing.md)** — the immutability the grade builds on. - **[Resolvers & Data Binding](./resolvers.md)** — how live data affects the grade. --- # The Capability Enforcer The enforcer is the on-chain component that makes ChainRender apps **safe by construction**. It walks a composition and either passes it (with an honest grade) or *refuses* it — so an unsafe app cannot be registered in the first place. Safety isn't a warning you might ignore; it's a gate you cannot pass. ## What it refuses (the hard rails) - **R1 — no arbitrary code.** An escape hatch to unanalyzable code refuses the whole composition. Unanalyzable = unsafe. - **R2 — no dishonest envelope.** An app that under-declares what it actually does (hiding a capability it reaches) is refused. You cannot claim to be safer than you are. - **R3 — no out-of-envelope actions.** An action requiring a capability the app never declared is blocked. - **R4 — no value smuggling.** An action that moves value while the app never declared `VALUE` is blocked — even if it tries to hide the value from its own capability set. - **R5 — no arbitrary calls.** An action whose `{to, selector}` isn't in the declared call set is blocked. A button can only do what it openly says it does. - **R6 — no off-envelope bindings.** A data binding needing an undeclared capability is blocked. - **R7 — dependency honesty.** Immutable deps grade A, attested B, unproven/mutable C — conservatively, never refused but always disclosed. ## Why refusal, not just warning A warning can be ignored, suppressed, or missed. For a UI that signs transactions, "safe unless you click through a warning" isn't safe. So the unsafe cases are *refused at registration* — they never become a live app. What remains is a system where *every registered app is structurally incapable* of the smuggling attacks above, and its grade honestly reflects whatever safe dependencies it does have. ## Drift-locked with the SDK The SDK's `grade()` computes the *same* result the on-chain enforcer does — a test asserts they never diverge. So the grade you see while building is exactly what the chain will enforce. No surprises at deploy time. ## Related - **[The Trust Grade](./trust-grade.md)** — what passes gets graded. - **[Actions & Signing](./actions.md)** — how declared actions satisfy R3–R5. --- # The Floor "The floor" is the single most important architectural principle in ChainRender. Understand it and you understand why the system can promise both rich, dynamic, GPU-accelerated experiences *and* absolute trustlessness — without compromise. ## The principle **Every ChainRender app has an on-chain floor: a complete, verifiable, fully on-chain rendering that requires nothing but the chain itself.** Anything beyond that floor — a GPU effect, a client-side animation, an off-chain fetch — is an **enhancement** layered on top, and every enhancement **fails closed to the floor.** ``` on-chain floor = the guaranteed, trustless, always-works rendering (Grade A) enhancement = an optional improvement (GPU, motion, fetch) that IMPROVES the floor but can NEVER be required for the app to work or be trustworthy ``` If an enhancement is unavailable — no WebGL2, a failed fetch, a dead gateway — the app doesn't break, doesn't degrade to garbage, and doesn't show a lie. It renders its floor: complete, correct, on-chain. ## Why this matters Most "on-chain" systems force a false choice: either be fully on-chain and *limited* (static, ugly, simple), or be rich and *depend on off-chain infrastructure* (a server, a GPU, a CDN) that reintroduces trust. The floor principle refuses that choice: - The **floor** is always fully on-chain, complete, and Grade A. It is what the app *is*. - The **enhancement** makes it richer when conditions allow. It is a bonus, never a dependency. A 3D SDF scene renders a GPU ray-march *if the client has WebGL2* — and a bit-exact, fully on-chain **poster** if it doesn't. Either way you see the scene; the GPU version is just sharper. The poster is the floor; the ray-march is the enhancement. The app never *needs* your GPU to be trustworthy or to work. ## How the floor keeps the grade honest This is why an enhancement doesn't drop an app's trust grade. A GPU effect over a complete on-chain poster **stays Grade A**, because the trustless floor is fully intact — the enhancement can vanish and nothing is lost. The grade measures the *floor's* trust properties; a fail-closed enhancement is free. Only an enhancement the output *actually depends on* (a live fetch with no on-chain fallback) drops the grade — because then there's no floor beneath it. ## The floor-vs-enhancement rule, stated plainly > **On-chain is the floor; the client (GPU, fetch, animation) is an enhancement that fails closed.** Every capability in ChainRender is designed against this rule: - **GPU rendering** → floor is the on-chain poster; GPU is the enhancement. - **Off-chain fetch** → floor is complete on-chain data; the fetch only *enriches* (and must be declared). - **Client animation** → floor is the static on-chain frame; motion is the enhancement. - **Live resolver data** → the read itself is on-chain (part of the floor); it fails closed to "unavailable." ## Why it's built this way Because trust cannot be conditional. "This app is trustless *if* your GPU works / *if* the CDN is up / *if* the gateway responds" is not trustlessness — it's a dependency wearing a trustless costume. By making the floor mandatory and complete, and every enhancement optional and fail-closed, ChainRender can be honestly trustless *and* genuinely rich. The floor is the promise; the enhancement is the delight. ## Related - **[The Trust Grade](./trust-grade.md)** — why a fail-closed enhancement stays Grade A. - **[The Render Pipeline](./render-pipeline.md)** — how fail-closed rendering works in practice. - **[Principles & Ethos](../resources/ethos.md)** — the floor as a founding value. --- # Presentation & Theming Presentation — colors, spacing, fonts, chart dimensions — is a **render-time parameter** in ChainRender. You can change how an app looks without redeploying it, at zero gas. This page explains how, and why it's separated from the app's immutable identity. ## The principle: skin is free, substance is baked *How an app looks* doesn't change what it *is* or *does*. So presentation is not part of the app's hash — it's supplied when the app renders, via a **presentation context**: ```ts renderHashWithContext(hash, { themeId, // which on-chain theme (colors/fonts/spacing) to apply palette, // chart/image palette override chartW, chartH, // chart dimensions }) ``` Change any of these and the *same app* (same hash) renders differently. No new deployment, no new identity, no gas beyond the read. A developer can iterate on the whole page's look — spacing, font size, colors — instantly and for free, and only commit a chosen theme on-chain once, when it's dialed in. ## Why this division exists Forcing a redeploy for a cosmetic tweak would be wasteful (gas per nudge) and meaningless (a color change doesn't alter what the app does). Worse, it would pressure developers to ship "good enough" rather than iterate to "right," because every iteration would cost. Making presentation render-time removes that friction entirely — which is exactly why the system draws the line here: **appearance is presentation (free, render-time); text, structure, and actions are identity (immutable, in the hash).** ## The token system Themes are built from **design tokens** — a W3C-aligned set of semantic values (colors, typography, spacing, sizing, borders, elevation, motion). Components reference tokens (`--cr-space-md`), not hardcoded values, so a theme change re-flows the whole page. Tokens are drift-locked between the on-chain generator and the SDK, and a **contrast invariant** ensures every theme meets WCAG AA — a low-contrast theme can't ship. Three presets ship: **light**, **dark**, and **high-contrast** (WCAG AAA). **Layout and type tokens (Enhanced mode).** Client 0.20's Enhanced mode adds a style registry (21 token families: layout, gap, pad, surface, tone, size, badge, marker, …) whose structure CSS uses only these theme variables, each with the light theme's value as its fallback, and never sizes text below the 12 px floor. It is VERSION_5 output: the interpreter is on Sepolia, Robinhood and Arc (OpcodeInterpreterV5Html, 2026-11-02), and the viewer and the builder's preview carry the structure CSS (docs/202 phase 3). The builder offers Enhanced only on a chain whose RenderRouter has the V5 interpreter. Classic (the default) is unchanged. See **[Style Tokens](../reference/style-tokens.md)** and docs/202. ## Related - **[Guides → Theme Without Redeploying](../guides/theme-without-redeploy.md)** — hands-on. - **[Content Addressing](./content-addressing.md)** — why substance is baked. --- # Actions & Signing An action is how a ChainRender UI lets a user sign a transaction. Actions are **declared, not arbitrary** — which is what makes it impossible for an app to hide a malicious transaction behind an innocent button. ## Declared actions A button carries an action *declaration*: a target address, a function signature, and an argument shape. It does **not** carry raw calldata. At sign time, the runtime *constructs* the calldata from the declaration: ```ts // the button declares: { to: "0x…", sig: "claim(uint256)", args: "$amount" } // the runtime constructs exactly: claim() // nothing else is expressible ``` The constructed transaction can **only** be the declared selector with the declared argument shape. There is no path to smuggle a different call, a different target, or a hidden value transfer. This is called **construct-for-sign**. ## Why not just carry calldata? Because raw calldata is opaque. A button labeled "Claim" could carry calldata for *any* call — drain a wallet, call a different contract, move 100× the displayed amount. By declaring the action structurally and constructing the calldata from the declaration, the transaction is *provably* what the button says it is. The enforcer's rails (R3–R5) guarantee the declaration is honest and in-envelope. ## Batches, signing, and value - **Batches** — a button can carry an ordered list of actions submitted atomically under one signature; each member is individually gated. One signature, N verified actions, no authority beyond their union. - **Typed signing** — EIP-712 typed-data signing is a declared capability (`SIGN_MESSAGE`), gated like any other. A sign message is signed **exactly as written**: nothing substitutes an input into it (only an action's `args` take `$name` values), so a `"$recipient"` there is signed as that literal text, or rejected by the wallet as a bad address. Write it with literal values that fit their declared types; `honestyFindings` reports `sign-message` otherwise. - **Value** — moving ETH requires the `VALUE` capability to be openly declared (rail R4). An app can't move value it never said it could. ## Related - **[The Capability Enforcer](./enforcer.md)** — the rails that keep actions honest. - **[Guides → Declare a Safe Action](../guides/declare-action.md)** — build one. ## Human-readable action decoding ("you see what you sign") Before a user signs an action, ChainRender renders a **plain-English decode of exactly what the transaction will do** -- derived from the *declared* action `{to, sig, value, args}`, not guessed from calldata. For example, a mint button decodes to: > **This button will:** Mint 3 -> 0x36a4...c3c9 - sends 0.03 ETH > function `mint(uint256)` on `0x36a4...C3c9` - **sends 0.03 ETH** Because the runtime can only execute *exactly* the declared action, the decode is a guarantee, not an inference -- the button cannot call anything else or send undeclared value. The panel states this explicitly. **Swaps and multi-arg financial actions** carry an honesty flag: because the raw parameters don't label which value is the slippage minimum or which addresses are the token path, the decode flags *"verify which value is your minimum-received and which token addresses are in the path."* This is deliberate -- a decode that looked complete while hiding the important half would be worse than none. ChainRender decodes *and* admits the limits of the decode. ## Payments build on actions A crypto **payment** is just a declared action (`pay(...)` / `transfer(...)`), decoded before signing like any other. To also serve users without crypto, an app can add an optional off-chain card on-ramp — honestly graded off-chain so the on-chain guarantee is never overstated. See [Payments](./payments.md) for the full pattern (crypto floor + card enhancement + honest grading). --- # Payments — Crypto Floor, Fiat On-Ramp Enhancement ChainRender lets developers add payments to their apps with the same discipline as everything else: the **on-chain crypto payment is the floor** (trustless, Grade A), and a **fiat/card on-ramp is an optional enhancement** (a convenience for non-native users, off-chain, honestly graded). ChainRender defines the payment *primitive* and the *honest trust signal*; the developer brings their own on-ramp / wallet SDK — ChainRender blesses no processor, the same way it blesses no oracle. ## Why not "card payments, fully on-chain"? A card payment fundamentally needs off-chain trusted parties (card networks, banks, chargebacks, KYC). Those are irreversible-vs-reversible and permissioned-vs-permissionless — the opposite of a blockchain. So a card payment can *never* be fully on-chain; there is always an off-chain processor. ChainRender does not pretend otherwise. Instead it makes the boundary explicit and honest. ## The model: a payment intent with two paths A payment in a ChainRender app is a **declared intent** — "the user needs to pay `amount` of `currency` to `destination` to unlock X" — resolved by one of two paths: | Path | What it is | Trust grade | |---|---|---| | **Crypto (the floor)** | A declared on-chain action (`transfer` / `pay`), signed by the user's wallet, decoded honestly before signing. | **A — Trustless.** Fully on-chain, no processor. | | **Fiat on-ramp (the enhancement)** | A card/bank payment via an external on-ramp (Stripe, MoonPay, Transak, Coinbase Onramp, etc.) that delivers the crypto / executes the on-chain result. | **C / ⚙ — off-chain convenience.** Clearly labeled; the app cannot claim this flow is fully on-chain. | This mirrors ChainRender's **floor-vs-enhancement** principle exactly: the trustless path is always present and is the default; the convenient off-chain path is optional and *fails closed to the floor* — if the on-ramp is unavailable, the user can still pay in crypto. ## How a developer expresses it A payment is **not a new on-chain node type** (the tag vocabulary is a closed on-chain set). It is a **pattern** built from the existing action model plus an honest capability flag: - **The crypto payment** is a `button` with a declared `action: { to, sig: "transfer(address,uint256)" | "pay(...)", value, args }` — exactly the declared-action model, decoded by the "you see what you sign" panel. - **The fiat on-ramp** is an `external` element (a link/embed to the developer's chosen on-ramp) that carries the existing **`CLIENT_FETCH` capability** — the same off-chain-dependency flag `external` media already uses. Because the on-ramp is a client-side, off-chain concern, it needs no new on-chain capability and no change to the immutable grading contract: it reuses the honesty machinery that already exists. An app that presents an on-ramp must declare `CLIENT_FETCH`; the validator rejects an app that has an off-chain dependency without declaring it (honesty is enforced, not optional). So a developer adds payments by: (1) a crypto payment action (the floor), and optionally (2) an on-ramp element with the `CLIENT_FETCH` capability (the enhancement). Both together give a non-native user a card path while the crypto path stays trustless. > **Design note.** The fiat path deliberately reuses `CLIENT_FETCH` rather than adding a new capability. The > capability bits are mirrored on-chain in the immutable grading contract, so a new bit would be an invasive on-chain > change — and it is unnecessary, because an off-chain on-ramp is exactly the kind of client-side off-chain > dependency `CLIENT_FETCH` already models. The primitive is a client-side pattern + the existing honesty machinery, > not a new on-chain type. ## The honest grading (why the ethos holds) The trust grade tells the truth about which path the user is on: - A crypto payment with a self-custody wallet → **A (Trustless)**. - A fiat on-ramp / hosted-wallet path → **C or ⚙** — the app *discloses* the off-chain trust rather than hiding it. This is the ethos in action: **marketing never exceeds what code enforces.** An app can offer card payments for onboarding, but it cannot be graded "fully on-chain" for that flow — the user sees the honest grade and knows exactly what they are trusting. The capability flag makes this non-optional: use an on-ramp, declare it, grade drops, truth is told. ## Bring your own provider (ChainRender blesses none) ChainRender defines the *intent* + the *grading*, not the provider. The developer chooses: - **On-ramp**: Stripe, MoonPay, Transak, Coinbase Onramp, Ramp, or any card→crypto provider. - **Wallet onboarding** (for users without a wallet at all): an embedded/smart-wallet SDK (email/social login → an invisible wallet), e.g. via account abstraction (ERC-4337 / EIP-7702). ChainRender's ProvenanceRegistry already verifies smart-account signatures (ERC-1271/6492), so account-abstracted users are first-class. - **Gas**: a paymaster can sponsor gas so a non-native user never buys ETH for gas separately. ChainRender stays provider-neutral: it gives the honest primitive and the honest grade; the developer plugs in the off-chain pieces their users need. ## The full non-native onboarding stack (developer-assembled) ``` Non-native user ├─ email/social login → embedded smart wallet (developer's wallet SDK; off-chain until funded) ├─ payment intent: "Pay $5" │ ├─ has crypto → on-chain action → Grade A (floor, trustless) │ └─ no crypto → card on-ramp → Grade C/⚙ (enhancement, declared CLIENT_FETCH) └─ gas sponsored by paymaster (developer's paymaster; invisible to the user) ``` The developer declares the intent and the honest capabilities; ChainRender renders it, decodes the crypto action, and grades every path truthfully. The user gets a card path; the on-chain guarantee is preserved for the crypto path and never overstated for the fiat path. ## Honest boundaries - **The card/on-ramp path is off-chain and is graded so.** ChainRender cannot make a card payment trustless — it can only make the boundary honest. - **ChainRender never custodies funds or processes cards.** The crypto action executes on the destination contract; the card payment executes at the developer's chosen processor. ChainRender renders and declares; it is not a PSP. - **The crypto floor is always the default.** The fiat path is an enhancement that fails closed to the crypto floor, never the only option — so an app is never *dependent* on an off-chain processor to function. ## Adding a payment in the visual builder The builder ships a **Payment** starter that seeds a working payment pattern: 1. In the builder, choose the **Payment (crypto floor + card on-ramp)** starter. 2. It creates: an amount input, a **"Pay with crypto"** button (the on-chain action — the trustless floor), an optional **"Pay with card"** link (the off-chain on-ramp), and an honest note. 3. **Wire the crypto action** — set the button's `action.to` to your payment contract and `action.sig` to your pay/transfer method (e.g. `pay(uint256)` or `transfer(address,uint256)`), with the amount as an arg. 4. **Wire the on-ramp** — replace the placeholder URL on the "Pay with card" link with your chosen provider's checkout URL (Stripe / MoonPay / Transak / Coinbase Onramp), passing the destination + amount. 5. The grade updates automatically: crypto-only stays **A (Trustless)**; adding the card on-ramp makes the app grade **C** — because the on-ramp link is tagged `provenance: "external"`, which the grader recognizes as an off-chain dependency and reflects honestly. You cannot present a card on-ramp and still grade A. If you drop the card link entirely, the app is a pure crypto payment and grades A. The card path is always optional and always falls back to the crypto floor. ## Related - **[Actions & Signing](./actions.md)** — the declared-action model the crypto payment uses. - **[Trust Grade](./trust-grade.md)** — how the honest grade classifies on-chain vs off-chain paths. - **[The Floor](./the-floor.md)** — the floor-vs-enhancement principle payments follow. --- # Composition & Mini-Apps Components compose. A ChainRender **app** is a composition of components — and compositions are themselves first-class, content-addressed, forkable objects. ## Composing You nest components (a box containing a header, tabs, and panels) and reference other registered components by hash. The composition's capability envelope is the **union** of its parts — you can't escape your components' declared capabilities by composing them, and you can't under-declare the aggregate. A composition is graded as a whole. ## Multi-view apps An app can have multiple **views** (tabs, wizard steps, master-detail, modal) with in-app navigation. Each view renders its own on-chain floor; the shell provides navigation. A reachability invariant ensures no view is stranded (unreachable), enforced on-chain. ## Mini-apps and forking A composition is a **mini-app manifest** — content-addressed, so it has a permanent identity, and **forkable**: a fork records its parent on-chain (App DNA / lineage), so you can trace where an app came from. This makes ChainRender apps composable building blocks, not one-off artifacts. Lineage is recorded in two registries, differently: an **app manifest** (`AppRegistry`) carries its `parent` inside the manifest, so the parent is part of the app's own content hash and `lineageOf` walks the ancestry; a **composition** (`CompositionRegistry.register(bytes, parent)`) stores the parent next to the bytes, set by whoever registers those bytes first (it is not part of the hash). A single **component** (`ComponentRegistry`) has no parent record: the builder's **Fork** reopens a deployed component for editing, and any change is simply a new component with a new hash. ## Related - **[Guides → Build a Multi-View App](../guides/multi-view-app.md)** - **[The Trust Grade](./trust-grade.md)** — how a composition's grade aggregates. --- # Privacy & Credentials ChainRender's privacy model is about **identity and claims**: proving something about a user ("verified member," "over 18," "holds this credential") *without revealing the underlying data*. This is distinct from transaction privacy (hiding value transfers), which is a separate layer. ## Reveal the claim, not the witness The core principle (structural data minimization): the render only ever sees the *result* of a verification, never the private input. Heavy verification happens off-chain (at attestation time) or via a zero-knowledge proof; on chain, the resolver reads only a boolean or a statement label. The interpreter, the RPC, and the rendered output never touch the witness. ## The built primitives - **Attested claims** — identity as attested statements; trust delegated to an ownerless attester registry. - **Selective disclosure** — member-of-set proofs (Semaphore-style, Groth16): prove you're in a set without revealing *which* member. - **Bound, single-use credentials** — non-transferable (bound to a holder) and single-use (a nullifier prevents replay), fail-closed on a tampered proof. These are unified behind a common convention (`IPrivacyResolver`) so an app gates on a *family* of privacy proof without knowing which circuit backs it. ## The built privacy primitives (in detail) - **`MemberSetVerifier`** — a Groth16 verifier for member-of-set proofs (Semaphore-style). A user proves they're in a set (an allowlist, a DAO, a credential set) **without revealing which member** — the proof's public signals are the set root + a nullifier, never the identity. - **`CredentialResolver`** — renders a selective-disclosure credential: a valid proof resolves to the checkmarked statement label; a tampered proof or wrong root renders "Unverified," never a badge. - **`BoundCredentialResolver` + `NullifierRegistry`** — bound (non-transferable, tied to a holder via the signal hash) and single-use (a nullifier prevents replay). Consuming the credential (spending the nullifier) is a separate, honestly-graded WRITE. A tampered proof, a wrong root, or an unknown statement all fail closed. - **`AttesterRegistry` / `AttestationAdapter`** — attested claims: identity as statements an attester vouched for, with trust delegated to the ownerless attester set (+ EAS via the adapter). - **`IPrivacyResolver`** — a unifying convention over the above, so an app gates on a *family* of privacy proof (member-of-set / attested-claim / selective-disclosure) without knowing which resolver backs it. ## Adding a new statement (the `zkAgeOver` family) — generic by construction The credential path is **generic**: `CredentialResolver` (kind `credential`) takes `abi.encode(statementId, a, b, c, pub)`, resolves the verifier for `statementId` from the **immutable** `StatementRegistry` (the caller never supplies the verifier — anti-spoof), calls `verifyProof` **on chain**, and renders `✓