kanmani143
components · api · one-line 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.

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.

73·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: 12 resolve, 73 do not, 1095 assert no payment, 12 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

adding a chain is one entry

The chain registry is a data file, and the conformance tests fail naming whatever is missing.

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

On licensing: the repository is source-available with no derivatives, which sits awkwardly beside a component layer whose whole purpose is to be embedded. That tension is real, it is written down in docs/14-GAPS.md, and it is not resolved here. Embedding embed.js by script tag is plainly intended and needs no licence grant, because it is a request to our server rather than a copy of our code.

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 →