Render a verdict in your own product, without being able to render it wrongly.
The data has always been open. The hard part is that a verdict is easy to display badly and displaying it badly is worse than not displaying it. Everything below makes the three available mistakes unavailable: a null cannot print as 0, unknown cannot wear the colour of false and there is no score to average the distinction away into.
build an agent, however you build
Two free checks do the work: one turns a draft into the exact register() transaction, the other tells you what an evidence document will be judged before you publish it. Every way in below reaches those two. Nothing here takes a key: the agent's own key signs.
Add the MCP server so the agent can call the checks as tools. Give it the guide as a skill or AGENTS.md so it follows the rules without being told.
claude mcp add --transport http kanmani https://kanmani.xyz/mcp # the guide as a skill mkdir -p ~/.claude/skills/kanmani && curl -s https://kanmani.xyz/skill.md > ~/.claude/skills/kanmani/SKILL.md
checked against docs.anthropic.com/en/docs/claude-code/mcp
codex mcp add kanmani --url https://kanmani.xyz/mcp # the guide, at your repo root curl -s https://kanmani.xyz/build/AGENTS.md >> AGENTS.md
checked against developers.openai.com/codex/mcp
// .cursor/mcp.json
{ "mcpServers": { "kanmani": { "url": "https://kanmani.xyz/mcp" } } }
# AGENTS.md at the repo root is read automaticallychecked against cursor.com/docs/context/mcp
// mcp_config.json
{ "mcpServers": { "kanmani": { "serverUrl": "https://kanmani.xyz/mcp" } } }checked against docs.windsurf.com/plugins/cascade/mcp
// .vscode/mcp.json
{ "servers": { "kanmani": { "type": "http", "url": "https://kanmani.xyz/mcp" } } }checked against code.visualstudio.com/docs/copilot/customization/mcp-servers
gemini mcp add --transport http kanmani https://kanmani.xyz/mcp
// or settings.json. httpUrl is streamable HTTP; url means SSE
{ "mcpServers": { "kanmani": { "httpUrl": "https://kanmani.xyz/mcp" } } }checked against github.com/google-gemini/gemini-cli docs/tools/mcp-server.md
# ~/.config/goose/config.yaml
extensions:
kanmani:
type: streamable_http
uri: https://kanmani.xyz/mcp
enabled: truechecked against goose-docs.ai/docs/getting-started/using-extensions
# config.yaml
mcp_servers:
kanmani:
url: "https://kanmani.xyz/mcp"
enabled: true
# then /reload-mcpchecked against NousResearch/hermes-agent docs/reference/mcp-config-reference.md
{ "mcpServers": { "kanmani": { "type": "streamableHttp", "url": "https://kanmani.xyz/mcp", "disabled": false } } }type must be streamableHttp in camelCase. Otherwise Cline falls back to SSE.
checked against docs.cline.bot/mcp/configuring-mcp-servers
// settings.json
{ "context_servers": { "kanmani": { "url": "https://kanmani.xyz/mcp" } } }checked against zed.dev/docs/ai/mcp
// opencode.json
{ "mcp": { "kanmani": { "type": "remote", "url": "https://kanmani.xyz/mcp", "enabled": true } } }checked against opencode.ai/docs/mcp-servers
# config.yaml
mcpServers:
- name: Kanmani
type: streamable-http
url: https://kanmani.xyz/mcpchecked against docs.continue.dev/customize/deep-dives/mcp
amp mcp add kanmani https://kanmani.xyz/mcp
checked against ampcode.com/docs/customize/mcp
curl -s https://kanmani.xyz/build/AGENTS.md > KANMANI.md aider --read KANMANI.md
Aider does not document MCP, so it gets the guide as a read-only conventions file and calls the HTTP API.
checked against aider.chat/docs/usage/conventions.html
https://kanmani.xyz/skill.md agentskills.io SKILL.md https://kanmani.xyz/build/AGENTS.md drop-in AGENTS.md https://kanmani.xyz/llms.txt for tools that only read llms.txt
checked against agentskills.io, agents.md, llmstxt.org
one line, for a directory that lists agents
Measured status instead of declared status, in somebody else's page. No build step, no framework, no styles escaping into your layout.
<script src="https://kanmani.xyz/embed.js" data-agent="10182"></script> <!-- light theme, for a light page --> <script src="https://kanmani.xyz/embed.js" data-agent="10182" data-theme="light"></script>
- It is sealed. The badge renders inside a closed shadow root, so it cannot inherit your CSS and cannot leak into it. That matters beyond tidiness: a badge that picked up a host page's error red could show unknown in the colour of an accusation.
- It fails quiet. A network error, a bad id or an unknown agent removes the element rather than drawing an error box in your page.
- Unchecked is not clean. An agent nobody has measured shows unchecked, never a passing state.
the components
Every one of these is rendering live data on this page right now, from the same functions our own pages use. If a component here is broken, our site is broken too.
<Verdict />
The four outcomes and a fifth state for never-checked. The title attribute carries the meaning, verbatim from the verifier.
import { Verdict, outcomeMeaning } from "@/components/kit.tsx";
<Verdict outcome="unknown" /> // renders the word, in slate, never red
<Verdict outcome={null} /> // "unchecked", which is not "clean"
outcomeMeaning("unknown")
// "We could not look or could not read what we fetched. This is never an accusation."<Count />
The load-bearing one, four lines long. There is no prop that makes a null render as a digit.
<Count value={73} /> // 73
<Count value={0} /> // 0 a measured zero is a number
<Count value={null} /> // unknown a failed read is a word<Ladder />
How far a claim got, as shape rather than score. Rungs not reached stay visible, so a reader sees what was not checked.
<Distribution />
Live, from this chain, right now: 75 resolve, 123 do not, 1095 assert no payment, 26 could not be checked. A segment below a floor width is still drawn, because a share that reads as zero when it is not is the error this project is about.
re-derive this
SELECT
count(*) AS verdicts,
count(*) FILTER (WHERE outcome IN ('true','false')) AS payment_claims,
count(*) FILTER (WHERE outcome = 'true') AS resolve,
count(*) FILTER (WHERE outcome = 'false') AS do_not_resolve,
count(*) FILTER (WHERE outcome = 'noClaim') AS no_claim,
count(*) FILTER (WHERE outcome = 'unknown' AND rung = 'declared') AS unknown_unreachable,
count(*) FILTER (WHERE outcome = 'unknown' AND rung = 'resolvable') AS unknown_bytes_moved,
count(*) FILTER (WHERE outcome = 'unknown' AND rung = 'intact') AS unknown_unparsed
FROM fovea.verdict_latestthe api
Open, CORS-enabled, no key. Every response carries checkedAt, checkedAtBlock and a reproduce field with the command that gets the same answer without us.
GET /api/agent/<id> one agent, including what is not known about it GET /api/claims?outcome=false&limit=50 filterable, free GET /api/sla/<agentId>?fromBlock=&toBlock= claim-truth rate over a block window POST /api/verify 402, then pay, then the verdict GET /feed.json every checked claim plus its distribution GET /schema.json the shape of every response GET /llms.txt the whole site as plain text, live numbers GET /agents.md the access contract, addressed to software GET /.well-known/agent-registration.json our own ERC-8004 record
Fields with no answer are null rather than absent, so nobody defaults them to zero. That is the one thing to know before consuming this by hand and it is the reason the components above exist.
curl -sS https://kanmani.xyz/api/agent/10182 | jq '.data.claims'
curl -sS 'https://kanmani.xyz/api/claims?outcome=false&limit=1' | jq '.data.total'
curl -sS https://kanmani.xyz/api/sla/10182 | jq '{rateBps, sample}' # rateBps is null on an empty sample, never 0Integration notes: How the Nansen integration works covers the endpoint, the data category, the request shape, the credits and a command block that reproduces the first-funder finding.
adding a chain is one entry
The chain registry is a data file and the conformance tests fail naming whatever is missing.
8 chains declared, 2 carrying ERC-8004 registries. The registries are per-chain singletons sharing one address set, so a new chain needs no new addresses. The coverage table →
// shared/erc8004-chains.json
{ "id": 130, "key": "unichain", "displayName": "Unichain",
"aliases": ["unichain", "uni"], "rpcEnv": "RPC_UNICHAIN",
"explorer": "https://uniscan.xyz",
"erc8004": { "startBlock": 1234567, "indexed": false } }
export RPC_UNICHAIN=https://…
npm test # conformance + drift and it names what is missinghow to actually get these
There is no npm package yet. An earlier version of this page showed import … from "@kanmani/kit", which was an import path nobody could resolve, exactly the kind of claim this project exists to check, on our own page. Until a package is published, the honest instruction is to copy components/kit.tsx from the repository. It is one file, has no dependency beyond React and carries the rule with it.
embed.js needs nothing. It is a script tag against a live origin, so the badge is available today whatever happens to the package question.
Licensing is resolved: copy it. components/kit.tsx and app/embed.js are released under MITthe only two files in the repository that are, since the rest is source-available with no derivatives. A component that exists to be rendered inside somebody else's product cannot be read-only and what the kit publishes is a constraint rather than an asset: a restrictive licence on a safety property works against its own purpose. The reasoning is in docs/24-KIT-LICENCE.md.
the one rule, if you read nothing else
unknown is never false and never 0.
These verdicts carry real operators' names. A failure on our side to look is not evidence that a payment did not happen and collapsing the two would let somebody else's outage publish as an accusation, at scale, automatically. Every component here enforces that so you do not have to remember it. How each rung is decided →