⚠️ Alpha · testnets only. ChainRender is in active alpha and deployed on testnets only — Ethereum Sepolia, the Robinhood Chain Testnet, and the Circle Arc Testnet — not on any mainnet. Contracts, addresses, and APIs may change. Do not use with real funds or production assets.
Guides

Grade B — a pinned-IPFS dependency (the honest off-chain one)

Grade A means everything the app renders from is on-chain. Sometimes you genuinely need an asset that's too big or too dynamic to store on-chain. Linking a plain URL drops you to C (Live) — the host can swap the bytes under your users. There is a better, honest middle: pin the asset so it's content-addressed and re-verifiable, and you earn B (Pinned).

B is one notch below A (retrieval is off-chain) and one above C (a mutable URL). You reach it only when the code actually enforces the pin — the grade is computed from what's enforced, never from a promise.

The three things B requires

  1. A raw CIDv1. Pin the file so its address is a raw CIDv1 — bafkrei…, sha256, a single block ≤ 1 MiB. That's the only shape a browser can re-hash and verify. A Qm… or bafybe… (dag-pb) hash is not accepted — it can't be verified client-side, so it would be dishonest to grade it B.

    ipfs add --cid-version 1 --raw-leaves ./emblem.svg   # → bafkrei…
    
  2. An on-chain pointer. Store the ipfs://<cid> in an on-chain record you control — an ENS contenthash. That's what separates B from C: the pointer can't be silently swapped.

  3. Client verification that fails closed. Before rendering, the app fetches the bytes, computes sha256, rebuilds the CIDv1, and checks it equals the on-chain CID. On any fetch failure, timeout, or mismatch it keeps the on-chain floor and says so — it never renders unverified bytes.

If any of the three isn't enforced, the honest grade is C (or a lie). All three, and it's B.

Declaring it to the grader

The SDK reads a pinnedDep and grades the dependency axis ATTESTED → B:

import { grade } from "@chainrender/client";

const pinnedDep = { protocol: "ipfs", cid: "bafkrei…" };   // raw CIDv1

grade(app, { pinnedDep }).letter;        // "B"  (word: "Pinned")
// or attach it to the app node — grade() picks it up either way:
grade({ ...app, pinnedDep }).letter;     // "B"

Fail-closed by construction: an unverifiable CID is ignored (the app stays at its on-chain grade, A) — you can't get a silent B. An explicit dependency option still wins if you set one. Through the MCP server, pass pinnedDep to chainrender_grade / chainrender_review.

isVerifiablePinnedDep(pinnedDep) tells you up front whether a CID qualifies, so you can warn the author before they build around it.

What to tell your users

Show the computed grade, not an adjective. A pinned-IPFS app is B (Pinned) — verifiable, but retrieved off-chain, and the breakdown names the pinned dependency as the reason. That's the honest trade-off: the user sees exactly what they're trusting before they act.