Bitpoort documentation

REST + MCP reference for AI agents. Bitpoort exposes 80 MCP tools and 328 REST paths (364 OpenAPI operations) live across Ethereum, Bitcoin, and Hyperliquid. Most surfaces are public and machine-readable — every example below is a real call you can paste.

Quick start

Three calls, no signup. The key endpoint is public; paste the returned bp_ key into the two calls that follow.

# 1 — free key, no signup (50 keys/IP/day, 90-day expiry)
curl -sX POST https://api.bitpoort.com/v1/keys

# 2 — first data call
curl -H "X-API-Key: bp_..."   "https://api.bitpoort.com/v1/whales/recent?chain=ETH&limit=3"

# 3 — screen an address against OFAC / UK HMT / IL NBCTF / JP METI
curl -X POST https://api.bitpoort.com/v1/forensic/screen   -H "X-API-Key: bp_..." -H "Content-Type: application/json"   -d '{"address":"0x098B716B8Aaf21512996dC57EB0615e2383E2f96","chain":"ETH"}'

Step 3 returns a live sanctions match on a designated Lazarus Group address (abridged — timestamps are ISO-8601 with microseconds and an explicit offset):

{
  "address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
  "chain": "ETH",
  "sanctions_status": "MATCH",
  "screening_status": "MATCH",
  "match_count": 2,
  "matches": [
    { "source": "OFAC_SDN", "list_id": "27307",
      "entity_name": "LAZARUS GROUP", "confidence": 1.0,
      "last_verified_at": "2026-08-20T03:01:07.332962+00:00" },
    { "source": "JP_METI", "confidence": 1.0, "metadata": {
        "aggregator": "OpenSanctions", "designating_authority": "JP_METI" } }
  ],
  "disclaimer": "Not legal advice. Verify with compliance counsel.",
  "_meta": { "tool_version": "1.2",
             "sources": ["IL_NBCTF", "JP_METI", "OFAC_SDN", "UK_HMT"] }
}

Note the address is echoed lower-cased, and matches can carry several designations for one address — count them from match_count, not from the presence of the array.

To plug the same surface into an agent, point your MCP client at https://mcp.bitpoort.com/ with an X-API-Key header — paste-ready configs for every supported host are at /agents.md.

REST API

Response envelope

Two response fields are contract but are not described in the OpenAPI schema — read them here.

coverage_status — is this answer trustworthy right now?

List and chain endpoints attach the freshness of the pipeline that served them. Treat data_fresh: false or a non-empty warnings array as a reason to caveat the answer, not to retry.

// healthy
"coverage_status": { "chain": "ETH", "tier": "live", "data_fresh": true,
                     "pipeline": "full", "node": "erigon-archive", "warnings": [] }

// degraded — the SHAPE returned when a chain's indexing pipeline trails
// its node. Illustrative: same chain as above, a different state.
"coverage_status": { "chain": "ETH", "tier": "stale", "data_fresh": false,
                     "pipeline": "pending", "node": "erigon-archive",
                     "warnings": ["... indexing pipeline is pending ..."] }

tier is one of live, stale, node-only, perps-only, unknown. pipeline is one of full, perps-only, pending, mixed, unknown. An unrecognised chain yields tier: "unknown" with an explanatory warning rather than an error — so always read the block, never assume a 200 means fresh data.

Pagination

Paginated endpoints return next_cursor and has_more alongside the rows. Pass the opaque next_cursor value back as ?cursor= to fetch the next page; stop when has_more is false (next_cursor is then null). Cursors encode sort position, so they stay stable while new blocks land — never construct one by hand.

_meta

Analytical responses carry _meta.sources (which registers were searched, or which pipelines produced the answer — not merely the ones that matched; per-match attribution is in matches[].source) and _meta.tool_version. Provenance is part of the answer: if a response cannot say where a number came from, treat it as unattributed.

Errors

Three different envelopes ship, and a 404 can be two of them. Handle all three — matching only on the nested shape is the most common integration bug.

# A. nested "error" object — 401, 403, 422, 429
{"error":{"code":"AUTH_REQUIRED",
          "message":"API key required. Get a free key instantly: curl -X POST https://api.bitpoort.com/v1/keys",
          "docs":"https://docs.bitpoort.com"}}

# 422 uses the same shape and adds a "details" array:
{"error":{"code":"VALIDATION_ERROR",
          "message":"limit: Input should be a valid integer, ...",
          "hint":"Check the parameter names and value ranges. ...",
          "docs":"https://docs.bitpoort.com",
          "details":[{"type":"int_parsing","loc":["query","limit"], ...}]}}

# B. flat — 404 on an unknown PATH
{"error":"NOT_FOUND","path":"/v1/token-flows","method":"GET",
 "hint":"This path doesn't exist. Browse the full path list at .../openapi.json"}

# C. wrapped in "detail" — 404 on an unknown or inapplicable CHAIN
{"detail":{"error":"UNKNOWN_CHAIN","received":"NOPE",
           "valid_chains":["ARB","AVAX","BASE","BTC","ETH","MATIC","OP","SOL"],
           "hint":"'NOPE' is not a valid chain ID. Valid: ..."}}
StatusCodeMeaning
401AUTH_REQUIREDNo key sent on an authenticated path.
401AUTH_INVALIDKey malformed, revoked, or past its 90-day expiry.
403Valid key, but the route needs a higher tier.
422VALIDATION_ERRORBad parameter. Nested shape plus a details array naming the offending field.
404NOT_FOUNDUnknown path. Flat shape (B); hint names the entry points.
404UNKNOWN_CHAINChain ID not recognised. detail-wrapped (C); carries valid_chains.
404CHAIN_NO_BLOCKSValid chain with no EVM blocks (Hyperliquid). Shape C; the hint names the tools to use instead.
400INVALID_CURSORCursor malformed or expired. Shape C; drop cursor and re-paginate from the start.
429Rate limit. See X-RateLimit-Reset (unix seconds) before retrying.

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds). These report the edge window currently applied to the caller, which is not the same number as your key's per-minute tier quota — that one is returned by POST /v1/keys as rate_limit. Back off on Remaining: 0 rather than polling.

MCP server

List the live catalog without installing anything. Note the Accept header — Streamable HTTP requires both media types, and omitting it is the most common setup failure:

curl -X POST https://mcp.bitpoort.com/   -H "X-API-Key: bp_..."   -H "Content-Type: application/json"   -H "Accept: application/json, text/event-stream"   -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Tools — 80 MCP tools across 12 lanes

Names and JSON schemas come from tools/list on the MCP endpoint; the same catalog with usage notes and per-lane counts is at /agents.md and, machine-readable, in /.well-known/mcp.json. What each lane is for:

Empty results are answers, not failures

An empty result is classified rather than returned bare, so an agent can tell “we searched and found nothing” apart from “we could not search.” Results carry is_successful_empty plus an empty_reason drawn from a fixed set.

Branch on the flag, not on the reason. is_successful_empty is authoritative: true means the lookup completed and the answer is genuinely nothing, false means the result is not usable as evidence, and null means the reason could not be established. A tool that knows better sets it explicitly; anything classified defaults to true, and unclassified always yields null. The reason tells you why:

empty_reasonRead it as
no_signalChecked to completion; nothing matched.
no_sanctions_matchScreened the sanctions registers; address is not listed. Negative evidence.
no_abuse_matchScreened community abuse lists; no report. Negative evidence.
no_mixer_matchNo mixer interaction found. Negative evidence.
unsupported_inputInput out of scope for this tool (wrong chain, wrong address type).
stale_pipelineThe backing pipeline is behind; the silence is not meaningful.
data_gapCoverage genuinely missing for that range. Do not read as “clean.”
unclassifiedEmpty, reason not established (is_successful_empty is null). Treat as inconclusive.

The three compliance reasons are deliberately distinct from no_signal: for a compliance officer, “we searched this register and the address is not on it” is a finding worth recording, and the headline names the register that was searched.

Authentication & tiers

Send X-API-Key: bp_... (or Authorization: Bearer bp_...). Free keys are issued instantly by POST /v1/keys, capped at 50 per IP per day, and expire after 90 days. The live figures below are served by /v1/billing/tiers (public) — note Compliance is tuned for signed-report workloads, not request volume, so its throughput is lower than Pro.

TierPriceRateDailyAdds
Free60/min10,000All 80 MCP tools, 90-day key
Pro$99/mo600/min1,000,000Managed chat, OAuth, dashboards, named keys
Compliance$1,000/mo100/min100,000Signed reports + continuous monitoring
Enterprisecustom5,000/minuncappedSales-led, not self-serve

81 REST operations need no key at all, including POST /v1/keys, /health, /v1/health, /v1/trust, /v1/whales/recent, /v1/feed/emerging-tokens, /v1/verify/{content_hash}, /openapi.json, /openapi-summary.json, and /redoc. The complete list ships in /openapi-summary.json.

Chain coverage

ChainParamStatusBacking
EthereumETHliveOwn Erigon archive node, full pipeline
BitcoinBTCliveOwn Bitcoin Core node, full pipeline
HyperliquidHLperps-onlyPerps market, traders, funding. No EVM blocks/v1/blocks/* returns 404 CHAIN_NO_BLOCKS; use the get_hl_* tools or /v1/feed/hl/*
AggregateALLliveCombines ETH + BTC + HL

Trap worth knowing: the block endpoints also accept ARB, AVAX, BASE, OP, and SOL as valid chain IDs, but no pipeline backs them — they return 200 with a null block and coverage_status.tier: "unknown", not an error. Check coverage_status rather than the HTTP status.

Sanctions screening is chain-agnostic and covers designations across BTC, ETH, TRON, LTC, XMR and others — the per-chain breakdown is live in /v1/trust.

Attestation & reproducibility

Discover programmatically