Docs
Citing btcintel
Every claim btcintel serves carries a citation you can quote and anyone can check later, without trusting us: the claim's id, the version you read, and the first sixteen hex characters of a hash over exactly what the source asserted. Versions never change; a claim that is later retracted or expires keeps its hash and its citation.
Ids and versions
| Kind | Id | What it says |
|---|---|---|
| annotation | ann_<n> | a label, category, abuse or role a source put on an address, transaction, block or script |
| attribution | att_<n> | a block attributed to a pool by a signature match or a manual row |
| relation | rel_<n> | a typed relation between two entities, never about a subject |
| event | evt_<n> | a dated event a source reported, with what it cites |
version starts at 1 and increments whenever the content hash changes. GET /v1/claims/{id} answers the current version; ?version=N pins an earlier one; /versions lists them all with their hashes, the actor and the time. A version is immutable: you can quote @v1 of a claim that is now at @v3 and a reader will fetch exactly what you saw. The same objects reach agents through the MCP tool get_claim (For agents).
The citation string
btcintel:<annotations|attributions|relations|events>:<id>@v<version> sha256:<first 16 hex of content_sha256>
btcintel:annotations:33932@v2 sha256:84ea2a24bc08df5eQuote it as it comes, in a footnote or a data field. The claim page (/claims/ann_33932) copies it in one click and shows every version.
What is hashed
The hash covers what the source asserted and nothing of our bookkeeping. For an annotation, the content object has exactly these fields:
| Content field | From the claim JSON |
|---|---|
kind | "annotation" |
subject | {kind, key} as given |
label, concepts, method, evidence | as given; concepts and evidence are [] when empty |
source_id | source.id |
fetch_sha256 | fetch.sha256 |
confidence_class | the stored class with its vocabulary prefix (confidence:trusted_provider); the claim shows it trimmed in confidence.class, and content carries it whole |
valid_from, valid_to, retracted_at, first_reported_at | validity.from, validity.to, validity.retracted_at, first_reported_at — RFC 3339 in UTC, to the second (2026-09-22T14:37:05Z), as content carries them; the optional ones only when present |
category, abuse, role, entity_id | as given, only when present; entity_id is entity.id |
Never hashed: claim_id, version, citation, content_sha256, created_at, updated_at, source.name and source.attribution, fetch.id, fetch.url and fetch.fetched_at, confidence.level, metadata, retraction_reason. An attribution's content object is {kind: "attribution", subject, entity_id, method, source_id, matcher_version, confidence_class, evidence} plus fetch_sha256, signature_id and the validity times when present; a relation's is {kind: "relation", about: "entity", from_entity, to_entity, type, source_id, confidence_class, evidence} plus fetch_sha256 and validity; an event's is {kind: "event", event_kind, occurred_at (a date), occurred_precision, title, source_id, cites, refs} plus summary, fetch_sha256 and retracted_at when present.
Serialisation and hash
The content object is serialised with the JSON Canonicalization Scheme (RFC 8785): object keys sorted by their UTF-16 code units, no whitespace, strings escaped exactly as JSON.stringify does, numbers written as ECMAScript writes them. The hash is SHA-256 over those bytes; content_sha256 is its lowercase hex and the citation carries the first sixteen characters.
Worked example
The Akira ransom payment recorded by Ransomwhere on transaction 71e4dae2…, claim ann_33932, version 2. Fetch it:
curl -s 'https://api.btcintel.io/v1/claims/ann_33932?version=2' | jq '{citation, content_sha256, label, subject, confidence, validity}'
{
"citation": "btcintel:annotations:33932@v2 sha256:84ea2a24bc08df5e",
"content_sha256": "84ea2a24bc08df5e30e45d5383de7ff1edbb8029fe532d980219242b4a25e24c",
"label": "Akira",
"subject": { "kind": "tx", "key": "71e4dae29ed1bc9fe008bba7f3542fd5961b780c220ca8778100a7cb8bd4c81b" },
"confidence": { "class": "trusted_provider", "level": 50 },
"validity": { "from": "2026-09-22T14:37:05.466118Z", "to": null, "retracted_at": null, "retraction_reason": null }
}The claim also carries content: the exact object the hash covers, as the table above describes it. Canonicalise it and hash it — here in Node (the same code runs in a browser):
import { createHash } from "node:crypto";
const claim = await (await fetch("https://api.btcintel.io/v1/claims/ann_33932")).json();
// RFC 8785: keys by UTF-16 code units, JSON.stringify escaping, ECMAScript numbers.
const canon = (v) =>
v === null || typeof v !== "object" ? JSON.stringify(v)
: Array.isArray(v) ? "[" + v.map(canon).join(",") + "]"
: "{" + Object.keys(v).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}";
const hash = createHash("sha256").update(canon(claim.content)).digest("hex");
console.log(hash === claim.content_sha256, claim.citation.endsWith(hash.slice(0, 16)));
// true trueA superseded version (?version=N below the current one) answers its hash and citation only; its content is not retained, so re-verification of an old version compares against the bytes you kept.
The canonical bytes begin
{"abuse":"abuse:ransomware","concepts":[],"confidence_class":"confidence:trusted_provider","entity_id":"storm-1567","evidence":[{"kind":"amount_sat","value":488528790},…and hash to 84ea2a24bc08df5e30e45d5383de7ff1edbb8029fe532d980219242b4a25e24c: the hash the API declared, and the sixteen characters the citation carries. Change one character of the label and the hash moves — that is what the citation pins. The comparison in the snippet uses plain string ordering; it equals UTF-16 order for keys without astral characters, which is every key btcintel writes.
When a citation stops matching
- The version moved. The current claim has a new
versionand hash; fetch the version you quoted with?version=Nand it still matches. - The claim was retracted. The version you quoted still verifies; the current version says why in
validity.retraction_reason. Say so when you cite it. - Nothing matches. Tell us: the feedback route takes the claim id and what you computed. A hash we cannot reproduce is a bug on our side, not yours.