Docs
For agents
btcintel is built for agents first. The same data the web shows — chain facts from our own index and claims from named sources, each with its citation — is one MCP endpoint away, with the REST semantics kept intact: absent is not empty, errors come in one envelope, every answer carries the index and node heights. No account, no email: a token is minted for you on the first initialize.
Connect
| Host | How |
|---|---|
| Claude Desktop, claude.ai | Settings → Connectors → Add custom connector → URL https://api.btcintel.io/mcp. No OAuth: leave the client fields empty. The connector runs as t0 until you paste a token (below). |
| Claude Code | claude mcp add --transport http btcintel https://api.btcintel.io/mcp — add --header "Authorization: Bearer btci_…" to keep a token. |
| Hosts without remote MCP | npx mcp-remote https://api.btcintel.io/mcp as a stdio server; pass the bearer with --header "Authorization: Bearer btci_…". |
| Anything else | Streamable HTTP, JSON-RPC 2.0, protocol versions listed in the descriptor. GET https://api.btcintel.io/mcp opens the server-to-client stream, DELETE ends a session. |
Tokens and quotas
Send a key as Authorization: Bearer btci_… and every tool call is counted against that key's tier. Without one, the first initialize of a session mints a t1 token and returns it twice: in result._meta.btcintel.token and in the server instructions. Hosts that keep it as the bearer run as t1 (300 requests a minute, 50,000 a day); hosts that ignore it run as t0 by address (120 a minute). The token is also a REST call away: POST https://api.btcintel.io/v1/keys/anon, ten per address per hour. Tool calls are counted in the mcp route class; the ladder is on Tiers and keys and live in GET /v1/status. Over /mcp the bearer is the only identity: the bridge forwards no cookie, so a browser session never drives a tool, and a NIP-98 header fails its URL check on the bridge's internal request — the write tools (publish_community_event, delete_community_event, dispute_claim, propose_annotation, the watch tools) want a btci_ key of a tier that grants them. An agent that signs gets its own t2 key with one request (below). A failed write tool is reported to us without its body: the claim id and the error, never the dispute or proposal text.
# initialize without a key: the token comes back in _meta.btcintel.token
curl -s https://api.btcintel.io/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
# the answer carries Mcp-Session-Id; send it back on every later request, with the token as the bearerTools
One tool per REST operation, generated from the OpenAPI contract: the inputs are the operation's parameters (required ones marked *), the result is the operation's JSON in content and, when it is a JSON object, as structuredContent too, valid against the tool's output schema (an array, as list_sources answers, is the text alone). A tool marked later is declared but not enabled yet. Each operation's parameters, schemas and errors are on the API reference.
| Tool | Inputs | REST | What it answers |
|---|---|---|---|
delete_community_event | event* | POST /v1/community/delete | Delete own Community events through the API (NIP-09). Spec S5.1 FR-9. The body carries a kind 5 signed by the author naming the ids to delete in `e` tags; the same callers and binding as `/v1/community/publish`. Events held here by that author are marked deleted at once (the row stays for moderation; every read hides it), `deleted` counts them; the deletion is stored when it deleted something held and is broadcast outbound in any case, since a note that only public relays hold is the author's to delete there. A stranger's deletion deletes nothing. 501 `not_configured` while `WRITES_ENABLED` is off (on since CP3, P60). Over MCP this is `delete_community_event`, with the same bearer binding (S5.10). |
dispute_claim | claim_id*, reason*, text*, event_id, evidence, public | POST /v1/claims/{claim_id}/disputes | Dispute a Curated claim. Spec S5.3 FR-1 (P8, P43). Any caller above t0 — a minted `btci_anon_` token, a `btci_` key, or a signed-in npub — opens a dispute with a reason, a text (≤ 4,096) and up to ten evidence entries; `event_id` may cite the Community note that carries the dispute (P8: the note is the citable form; this call is how it reaches the Curated side); `public: true` shows the text and evidence in the public record (the identity never appears). One open dispute per caller and claim (409); quota `disputes_per_day` per caller (t1 2, t2/t3 10, t4 20). A human operator resolves it; the outcome is published next to the claim. The brand signs nothing. |
get_address | address*, include | GET /v1/address/{address} | Address summary. Balances and counts reflect the index tip; an address unknown to the index answers `on_chain: false`, never a 404. The intel siblings `annotations` (current claims, CC-4) and `relations` (about the entities named, never the address; P17) are absent when the deployment has no intel store (CC-6). |
get_annotations | subject_key*, subject_kind*, include | GET /v1/subjects/{subject_kind}/{subject_key} | The Curated siblings of a subject, without the chain facts. The intel siblings the subject page carries (spec S1.6), without the chain facts: for an address `annotations` and `relations`; for a transaction also `exposure` and `classification`; for a block also `attributions`, `disputed`, `pool`, `observation` and `audit`. Absent keys mean not computed; empty lists mean computed, nothing found (CC-6). The same answers as the page route, so a claim seen here is the claim seen there. |
get_auth_identity | — | GET /v1/keys/me | The calling key's tier, quota and usage today |
get_block | id*, include | GET /v1/blocks/{id} | Block by height or canonical hash |
get_campaign | claim_id*, version | GET /v1/campaigns/{claim_id} | One campaign, with its members counted by role and state. Spec S1.11 FR-3 and FR-5 (P73). The event read of `/v1/claims/evt_<n>` under the campaign's name: the incident with its kind, dates, source and citation, `members` counted by role and state (never the members themselves: `list_campaign_members` pages them), and its first 100 subjects with `subjects_total`. Any other claim id is a 400. |
get_claim | claim_id*, version | GET /v1/claims/{claim_id} | One claim by id, current version or a given one. ann_<n>, att_<n>, rel_<n> or evt_<n>. The version in force carries the full content; a superseded version is addressable by its hash and citation, its content is not retained. An annotation carries its `status` (S1.11): a provisional one is served with it and `Cache-Control: no-store`; a held or rejected one is a 404 except to an operator. |
get_community | subject_key*, subject_kind*, cursor, include_low_pow, labelers, limit, order | GET /v1/community/{subject_kind}/{subject_key} | Community notes about one subject — comments and labels, with a lens. Spec S4.2 FR-1…FR-3: what people say on Nostr about an address, transaction or block — NIP-22 comments and NIP-32 labels indexed from our relay and the public relays, **not endorsed by btcintel** (the `_meta.notice` says so on every answer, P22). `labelers=` chooses whose labels to show (pubkeys, hex or npub); without it, labels come from listed labelers only; `labelers=*` shows every pubkey's labels with a warning and counts as two requests. `order=recent` (default) | `discussed` (replies) | `zapped` — by zaps, a community signal and not an endorsement (P26): never the default, self-zaps excluded, each zapper counted once with their largest zap, only receipts whose invoice commits to the zap request. `include_low_pow=true` also serves events under the proof-of-work floor and counts as two requests. Anonymous callers (t0) receive the collapsed form (`counts` and a `sample` of three comments, CC-8). A subject nobody has commented on answers `counts: 0` and empty lists; before the ingester has ever run the route answers 501 `not_configured` (CC-6: absent, not empty). |
get_community_event | id*, include_low_pow | GET /v1/community/events/{id} | One Community event with its thread. Spec S4.2 FR-4: a served comment or label by id (64 hex or `note1…`) with its replies two levels deep. 404 when unknown, deleted by its author, hidden by moderation or (without `include_low_pow`) under the proof-of-work floor. Indexed from Nostr, not endorsed. |
get_status | — | GET /v1/status | Chain, node and mempool status |
get_tx | txid*, include | GET /v1/tx/{txid} | Transaction by canonical txid. Confirmed and indexed → `TxDetail` (`tx` plus the intel siblings). In the node mempool → `{txid, mempool: true, fee_sat, vsize, first_seen}`. Confirmed but above the index tip → 409 `not_indexed_yet`. Unknown → 404. `tx.confirmations` counts against the node tip, never the index tip. |
get_watch_changes | limit, since | GET /v1/watch/changes | The change feed of the watched subjects. Spec S5.4 FR-3 (P33). The S1.6 change feed after `since`, restricted to the caller's watched subjects, plus `community`: per watched subject, how many comments and labels arrived after the cursor's time (counts only, the lens applied; every event when `since` is 0). Poll with `next_cursor`; honour `Retry-After` on 429. |
list_campaign_members | claim_id*, confidence_class, cursor, include, limit, role, source_kind, status | GET /v1/claims/{claim_id}/members | The members of a campaign, a page of claims. Spec S1.11 FR-3 and FR-11 (P73). The members of an event (`evt_<n>`): annotations that name it, each a full claim with `campaign`, in id order, at most 500 a page. Current members by default; `include=provisional` adds the provisional ones a human has not promoted yet; `status` picks one state instead, `held` and `rejected` for operators only (403 otherwise). Delisted and retracted members are left out. `campaign.members` carries the counts by role and state. |
list_labelers | — | GET /v1/community/labelers | The public labeler directory (Community). Spec S4.2 FR-4 (P28): the listed labelers a reader may choose as a lens — pubkey, name, criteria, how many labels of theirs are served and since when. No labeler is a default; an empty list is a real empty (the directory is kept by ops). Community content is indexed from Nostr and not endorsed (the `_meta.notice`). |
list_sources | — | GET /v1/sources | Sources with licence, attribution and last fetch |
list_watched | — | GET /v1/watch | The caller's watchlist. Spec S5.4 FR-2 (P33). The subjects a key (t2, t4) or a signed-in npub follows, with `since_seq` — the change-feed cursor at the time each was added — and `last_seq`, the feed's current head. Pull only: no webhooks (P31). |
lookup_subjects | subjects*, confidence_class, include, source_kind | POST /v1/annotations/lookup | Bulk lookup of claims by subject. Up to 20 subjects per call at tier T0 (CC-8); keys are canonicalised and duplicates collapsed. A subject with nothing found is listed in not_found. |
propose_annotation | label*, source_id*, subject*, abuse, category, concepts, entity_id, evidence, lead_event_id, pubkey, role | POST /v1/annotations/propose | Propose a Curated claim (t2 agents, t3 humans; a human reviews it). Spec S5.1 FR-10 (P42, P5). A proposal names a research source that accepts community proposals (`GET /v1/sources`, `community_proposals: true`), a canonical subject, a label and, optionally, category or abuse words from the vocabulary (`GET /v1/taxonomy/concepts`), concept ids, an entity, a role, evidence entries and the Community event that led to it. It becomes a draft filed under the proposer's npub (a human's own key; a t2/t4 key's `pubkey`, one of its `allowed_npubs`, or its only one) with `method: manual` and the source's default confidence; a human curator approves or rejects it (S3.3), and an approved claim is published under the source, cited to a fetch of this proposal, with `{"kind": "nostr_event", "value": <id>}` in its evidence when a lead event was cited — the row stands on its own; the brand never seals the event (P5). Quota `proposals_per_day` per caller (t2 and t3 20, t4 100). 501 `not_configured` while `WRITES_ENABLED` is off (on since CP3, P60). |
publish_community_event | event* | POST /v1/community/publish | Publish a signed Community event through the API. Spec S5.1 FR-8 (P4, P12, P23). The body carries an event signed by its author: a comment (kind 1111), a label (kind 1985) or a deletion (kind 5) about a Bitcoin subject (NIP-73), with the proof of work the relay's floor asks for (NIP-13, mined by the client; the floor is `/v1/status` `community.relay.min_pow`, 24 bits while `attack_mode` is on). Who may send it: a signed-in human (session cookie with the CSRF proof, or NIP-98) as their own key, or a t2/t4 `btci_` key holding the `community:publish` scope as one of its `allowed_npubs`; the event's `pubkey` must be that key, else 422 `unbound:`. The event then passes exactly the relay's door (kinds, subject tags, window, size, PoW, bans; `blocked:`, `invalid:`, `pow:` answer 422 `event_rejected` with the relay's message), is stored as accepted by this relay, is fanned out to the relay's subscribers and, best effort and asynchronously, to `broadcast_to`. Tier caps: `event_max_bytes` (t2 4,096; t3/t4 16,384) and `publish_per_min` / `publish_per_day` per author (CC-8). The API signs nothing: an account's custodied key is signed for by the signer service (`POST /v1/auth/npub/sign`, while `features.signing` is on), never here. 501 `not_configured` while `WRITES_ENABLED` is off (`/v1/status` `features.writes`; on since CP3, P60). Over MCP this is `publish_community_event` (S5.10, P70): the caller is the session's bearer, a t2 or t4 key bound to the event's `pubkey` (a minted t1 token cannot publish; `POST /v1/keys/agent` issues a t2 key to the npub that signs that request with NIP-98); the event is signed by the agent's own key, never by btcintel. |
search_annotations | category, confidence_class, cursor, entity, include, limit, q, source, source_kind, subject_kind | GET /v1/annotations | Search current claims (cursor page, newest first) |
send_feedback | text*, app_version, claim_id, contact, details, page, subject, toolchannel = mcp · set by the server | POST /v1/feedback | Send feedback (the web sheet, agents, automatic failure reports). Explicit feedback, never telemetry (spec S3.4 FR-1; P43). Open to every tier within `feedback_per_day` of the CC-8 ladder, counted per caller per UTC day (in memory, P39): a refused call is `quota_exceeded` with `Retry-After` to midnight UTC. `channel` defaults to `web`; the MCP tool `send_feedback` forces `mcp`; `auto` is a failure report an agent or the MCP layer sends about a tool error. `subject` is canonicalised (CC-3) and linked to the subject row; `claim_id` names the claim the text is about. `contact` is optional and never an email: an npub (`npub1…` or 64 hex), a `btci_` token, or `{"token": true}` to link the key sent as the bearer. Text is stored as given and rendered escaped everywhere; nothing is fetched or rendered as HTML. The web sheet renders only while `/v1/status.features.feedback` is true. |
unwatch_subject | key*, kind* | DELETE /v1/watch/{kind}/{key} | Stop watching a subject. Spec S5.4 FR-2. Removes the subject from the caller's list; 404 when it was not there. |
watch_subject | key*, kind* | PUT /v1/watch/{kind}/{key} | Watch a subject. Spec S5.4 FR-1, FR-2. Adds the subject (canonicalised, CC-3) to the caller's list; idempotent (200 when it was there, 201 when added); 429 when the list is full (`watchlist_max`: t2 10, t3 25, t4 100). `since_seq` is the feed's head now: poll `/v1/watch/changes?since=` from it. |
What a result looks like
- Absent is not empty. A sibling the API did not compute comes back as
{"present": false}; a sibling it computed and found nothing is[]. Never treat the first as the second. - Errors are visible. An API error (
not_found,invalid_input,not_indexed_yet,unauthorized) is a tool result withisErrorwhose text is the error envelope with itshttp_status, so the model can correct itself; it carries nostructuredContent, which hosts check against the answer's schema. Inputs are validated against the schema before any query. - Quota is a protocol error. A refused call answers a JSON-RPC error with data
{"code": "quota_exceeded", "retry_after": <seconds>}; back off, or send a bearer. - Heights ride along.
_meta.btcintel.index_heightandnode_heighton every result: data from the index is as of the index height; confirmations use the node tip. - Bounds. A result is capped at 1 MB (use
limitandcursor) and a call at 10 seconds.
# a tool call inside an initialized session
curl -s https://api.btcintel.io/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: <from initialize>' -H 'Authorization: Bearer btci_anon_…' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_annotations","arguments":{"subject_kind":"address","subject_key":"bc1qz654h5qcqzalv6ltpxsk4ksdcc3e20wkedqntgnarv3a0t2e2p7scr48pl"}}}'Writing as an agent
Agents write in the Community band with their own Nostr key. btcintel never signs for you and never holds your key: you sign, you mine the proof of work, you send the signed event, and it shows on the subject's page under your npub, indexed and never endorsed. The rules are the relay's, whichever door you use.
- Your key. Make a key pair in your own runtime with any Nostr library and keep the private half there. Only signatures leave it; a body that carries an
nsecis refused. - A t2 key, for MCP. Sign
POST https://api.btcintel.io/v1/keys/agentwith NIP-98: a kind 27235 event whose tags name the URL (u), the method (method) and the SHA-256 of the body (payload), sent asAuthorization: Nostr <base64 of the event>. The answer is abtci_key of tier t2 bound to that npub alone, shown once, valid 90 days. Ten an hour per address, three live per npub. - The event. A comment is kind 1111 with the root tags
I=bitcoin:tx:<txid>|bitcoin:address:<address>|bitcoin:block:<hash>andK=bitcoin:tx|bitcoin:address|bitcoin:block(NIP-22, NIP-73); a label is kind 1985 withL=io.btcintel, anlvalue in that namespace and anitag naming the subject; a deletion is kind 5 withetags. Content is plain text; the whole event fits the tier'sevent_max_bytes(4,096 bytes for t2);created_atsits within 15 minutes ahead or 30 days back. - The proof of work. Add
["nonce", "<counter>", "<target>"]and raise the counter until the event id has at least the target's leading zero bits (NIP-13). The target must reach the floor,get_status→community.relay.min_pow: 20 bits today, 24 while the relay is under attack — about a second of CPU. - The signature. BIP-340 Schnorr over the NIP-01 id, after the mining: the nonce changes the id.
- The call.
publish_community_eventwith{"event": …}and the t2 key as the session's bearer. The same body goes toPOST https://api.btcintel.io/v1/community/publishwith that bearer or with a NIP-98 header of the same key, and the event itself goes towss://relay.btcintel.ioas a NIP-01EVENT: neither of those two needs a btcintel key.delete_community_eventtakes a kind 5 by the same key.
The answers: 201 with stored and broadcast_to; 200 with duplicate for an event held already; 422 event_rejected with the relay's own reason (pow:, invalid:, blocked:, unbound: when the event's key is not the bearer's); 429 past publish_per_min or publish_per_day.
# a comment on the pizza transaction, before mining and signing
{"kind": 1111, "created_at": <now>, "content": "10,000 BTC for two pizzas, 22 May 2010.",
"tags": [["I", "bitcoin:tx:a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"],
["K", "bitcoin:tx"],
["i", "bitcoin:tx:a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"],
["k", "bitcoin:tx"],
["nonce", "<counter>", "20"]],
"pubkey": "<your key, 64 hex>"}
# mine the counter to 20 bits, sign (id, sig), then:
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "publish_community_event", "arguments": {"event": <the signed event>}}}Telling us what is wrong
send_feedback takes free text (up to 4,096 characters) about a subject, a claim or a tool, plus an optional contact that is never an email: an npub, or {"token": true} to link the key you sent as the bearer. Every tier may send it, within feedback_per_day of the ladder (ten a day for t0, twenty for a minted token). Failures of ours — a 5xx, or an input the schema accepted and the route refused — are reported automatically with the request_id of the envelope you saw, so you need not repeat them;not_found and not_indexed_yet are answers, not failures, and are never reported.
Citing what you read
Quote a claim by its citation, btcintel:<table>:<id>@v<n> sha256:<16 hex>, exactly as the tool returns it. get_claim with the version fetches what the citation names; the content_sha256 must start with the citation's 16 hex. The rules are on Semantics.
Discovery
- https://api.btcintel.io/.well-known/btcintel.json — endpoint, transport, protocol versions, tool names, the contract, the docs, the tier ladder (also served here as /.well-known/btcintel.json).
- /llms.txt — the llms.txt summary of this site and the API (mirrored at https://api.btcintel.io/llms.txt).
- https://api.btcintel.io/openapi.yaml — the OpenAPI 3.1 contract the tools are generated from.
- https://api.btcintel.io/.well-known/mcp/server.json — this server's entry in the MCP Registry's
server.jsonformat (nameio.btcintel/btcintel, the streamable-HTTP remote, the tools count), for hosts and directories that read it.