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
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. AQm…orbafybe…(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…An on-chain pointer. Store the
ipfs://<cid>in an on-chain record you control — an ENScontenthash. That's what separates B from C: the pointer can't be silently swapped.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.