build an agent · components · api · embed

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 Codemcp
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 CLImcp
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

Cursormcp
// .cursor/mcp.json
{ "mcpServers": { "kanmani": { "url": "https://kanmani.xyz/mcp" } } }

# AGENTS.md at the repo root is read automatically

checked against cursor.com/docs/context/mcp

Windsurfmcp
// mcp_config.json
{ "mcpServers": { "kanmani": { "serverUrl": "https://kanmani.xyz/mcp" } } }

checked against docs.windsurf.com/plugins/cascade/mcp

VS Code (Copilot)mcp
// .vscode/mcp.json
{ "servers": { "kanmani": { "type": "http", "url": "https://kanmani.xyz/mcp" } } }

checked against code.visualstudio.com/docs/copilot/customization/mcp-servers

Gemini CLImcp
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

Goosemcp
# ~/.config/goose/config.yaml
extensions:
  kanmani:
    type: streamable_http
    uri: https://kanmani.xyz/mcp
    enabled: true

checked against goose-docs.ai/docs/getting-started/using-extensions

Hermes Agentmcp
# config.yaml
mcp_servers:
  kanmani:
    url: "https://kanmani.xyz/mcp"
    enabled: true
# then /reload-mcp

checked against NousResearch/hermes-agent docs/reference/mcp-config-reference.md

Clinemcp
{ "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

Zedmcp
// settings.json
{ "context_servers": { "kanmani": { "url": "https://kanmani.xyz/mcp" } } }

checked against zed.dev/docs/ai/mcp

OpenCodemcp
// opencode.json
{ "mcp": { "kanmani": { "type": "remote", "url": "https://kanmani.xyz/mcp", "enabled": true } } }

checked against opencode.ai/docs/mcp-servers

Continuemcp
# config.yaml
mcpServers:
  - name: Kanmani
    type: streamable-http
    url: https://kanmani.xyz/mcp

checked against docs.continue.dev/customize/deep-dives/mcp

Ampmcp
amp mcp add kanmani https://kanmani.xyz/mcp

checked against ampcode.com/docs/customize/mcp

Aideragents-md
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

Anything elseskill
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>
kanmani·#10182answered1 of 42 claims resolvekanmani·#9999goneunchecked
  • 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.

truefalseno claimunknownuncheckedfalse
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.

123·unknown·0← a real zero still reads as 0
<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.

declaredparsedtrueunchecked

<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_latest

the 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 0

Integration 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 missing

how 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 →