Documentation

Everything needed to connect an MCP client, call explain_transaction, parse the response, and pay per call, by hand or autonomously.

Introduction

0200project builds machine-native tools for autonomous AI agents: narrowly scoped services with strict JSON contracts, designed to be called and parsed by software without a human in the loop. The first and currently only shipped tool is base-transaction-decoder, launched August 20, 2026. More tools are in development.

Deterministic by design. There is no LLM in the response path. Every response is produced by deterministic onchain decoding: the same input always produces the same output. Nothing is generated, so nothing can hallucinate or drift between calls.

MCP is the interface. Each tool is served over the Model Context Protocol as a streamable HTTP endpoint. Any MCP client can connect; any HTTP client can call it directly with a JSON-RPC request. The registry entry is io.github.0200project/base-transaction-decoder.

For agents / machine-readable. The HTTP contract is published as OpenAPI at api.0200project.com/openapi.json, and a plain-text guide for agents lives at api.0200project.com/llms.txt. The same decode is also available as plain REST, POST /explain with {"tx_hash": "0x…"}, identical output, standard x402 flow.

Quickstart

The server is one streamable-HTTP endpoint: https://api.0200project.com/mcp. Pick a client, every path calls the same tool.

REST

The simplest path, no MCP client, no SDK. POST a transaction hash, get the explanation back as plain JSON:

terminal
curl -X POST https://api.0200project.com/explain \
  -H 'Content-Type: application/json' \
  -d '{"tx_hash":"0x0c84b951051f779903b57af9225ca570c77cd5531195968dd78106a69d6c4d8c"}'

The first calls are free. Past the free tier the endpoint answers with a standard HTTP 402 x402 challenge, pay $0.02 in USDC on Base and retry, or use an x402-capable HTTP client that handles it automatically. For heavy use, the $9 pass covers 10,000 calls over 30 days via the X-BTX-Pass header.

Claude Code

One command adds the server over streamable HTTP:

terminal
claude mcp add --transport http base-transaction-decoder https://api.0200project.com/mcp

Then ask Claude about any Base transaction hash, it calls explain_transaction on its own.

Claude Desktop

Add the server under mcpServers in claude_desktop_config.json, then restart Claude Desktop:

claude_desktop_config.json
{
  "mcpServers": {
    "base-transaction-decoder": {
      "type": "streamable-http",
      "url": "https://api.0200project.com/mcp"
    }
  }
}

The config file lives at:

  • macOS — ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows — %APPDATA%\Claude\claude_desktop_config.json

Cursor

Create .cursor/mcp.json in the project root (or ~/.cursor/mcp.json to enable it in every project):

.cursor/mcp.json
{
  "mcpServers": {
    "base-transaction-decoder": {
      "type": "streamable-http",
      "url": "https://api.0200project.com/mcp"
    }
  }
}

TypeScript

Uses the official @modelcontextprotocol/sdk (npm install @modelcontextprotocol/sdk). One free-tier call:

client.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const client = new Client({ name: 'my-agent', version: '0.0.1' });
await client.connect(
  new StreamableHTTPClientTransport(new URL('https://api.0200project.com/mcp')),
);

const result = await client.callTool({
  name: 'explain_transaction',
  arguments: { tx_hash: '0x0c84b951051f779903b57af9225ca570c77cd5531195968dd78106a69d6c4d8c' },
});
console.log(result.structuredContent);

await client.close();

Python

Uses the official mcp Python SDK (pip install mcp). One free-tier call:

client.py
import asyncio

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

TX = "0x0c84b951051f779903b57af9225ca570c77cd5531195968dd78106a69d6c4d8c"

async def main() -> None:
    async with streamablehttp_client("https://api.0200project.com/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("explain_transaction", {"tx_hash": TX})
            print(result.structuredContent)

asyncio.run(main())

curl

No MCP client needed, POST a JSON-RPC tools/call envelope directly:

terminal
curl -X POST https://api.0200project.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"explain_transaction","arguments":{"tx_hash":"0x0c84b951051f779903b57af9225ca570c77cd5531195968dd78106a69d6c4d8c"}}}'

The response arrives as a server-sent-events frame, an event: message line followed by a data: line carrying the JSON-RPC result:

response · text/event-stream
event: message
data: {"jsonrpc":"2.0","id":1,"result":{ ... }}

x402 paid client

The autonomous-payment path: the client makes the call, catches the in-band 402 challenge, signs a USDC payment, and retries, no account, no API key. The wallet needs USDC on Base; the exact scheme uses an EIP-3009 authorization, so it needs no ETH for gas. Use a dedicated wallet holding a small balance, never a personal one.

Packages: npm install @modelcontextprotocol/sdk @x402/mcp @x402/evm viem. Excerpted from the runnable test client at scripts/paid-call.ts:

paid-call.ts
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { ExactEvmScheme } from '@x402/evm/exact/client';
import { createx402MCPClient } from '@x402/mcp';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(process.env.X402_TEST_PRIVATE_KEY as `0x${string}`);

const client = createx402MCPClient({
  name: 'my-paying-agent',
  version: '0.0.1',
  schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }],
  onPaymentRequested: async ({ paymentRequired }) => {
    const a = paymentRequired.accepts[0];
    console.log(`402 received: ${a?.amount} units of ${a?.asset} to ${a?.payTo}. Paying...`);
    return true; // approve the $0.02 payment
  },
});

await client.connect(new StreamableHTTPClientTransport(new URL('https://api.0200project.com/mcp')));

const result = await client.callTool('explain_transaction', {
  tx_hash: '0x0c84b951051f779903b57af9225ca570c77cd5531195968dd78106a69d6c4d8c',
});
console.log(`paid=${result.paymentMade} isError=${result.isError ?? false}`);

await client.close();

Whichever path you take, the tool result carries the explanation twice: as structuredContent, and stringified in content[0].text. Parse whichever your client prefers.

The free tier is metered per client, one IPv4 address, or one IPv6 /64. The first 50 calls in any 24 hours are free with no signup, counted per IP address (IPv6 collapses to the /64), machines behind one NAT or a shared CI egress draw from a single counter, which is why the paywall can appear before 50 of your own calls. The window is rolling: a shared address that runs out recovers the next day rather than staying walled. After that, each call is $0.02 via x402.

Installation

Hosted endpoint

The hosted server at https://api.0200project.com/mcp works with any MCP client using the config in the quickstart — nothing to install. It is also listed in the MCP registry as io.github.0200project/base-transaction-decoder.

Self-hosting

The server is open source under an MIT-style license and runs anywhere Docker runs. It is a single-writer service: the free-tier counters, the passes and the event log are files on one machine’s disk, so it runs as one instance rather than a scaled pool. That is a deliberate constraint rather than a stage we have not outgrown, a settled engagement cannot be charged a second time, because the guard that refuses a repeat reads state only one process writes. Two instances would split that state: the free allowance and the rate limit would both double, and a pass minted on one would be unknown to the other.

terminal
git clone https://github.com/0200project/base-tx-explain
cd base-transaction-decoder
npm install
cp .env.example .env
npm run dev

Defaults are free mode with public Base RPCs. Key environment variables:

VariablePurpose
PAYMENT_MODEnone or x402. Default is free mode.
X402_PAY_TOAddress that receives x402 payments.
X402_PRICE_USDPrice per paid call, in USD.
X402_FACILITATOR_URLx402 facilitator used to verify and settle payments.
FREE_CALLS_PER_IPFree calls granted per client before payment is required.
BASE_RPC_URLSBase RPC endpoints. Defaults to public Base RPCs.

Authentication

There is none, by design. No accounts, no API keys, no OAuth, nothing to provision, rotate, leak, or revoke.

Identity is replaced by payment. A free tier is metered per client, one IPv4 address, or one IPv6 /64, and beyond it each call is paid individually over x402. The server never needs to know who you are, only that the call is paid for.

base-transaction-decoder

One tool: explain_transaction(tx_hash) returns a strict JSON explanation of any Base mainnet transaction (chain id 8453), a plain-English summary, a classified action type, every asset that moved, labeled counterparties, evidence-backed risk flags, and the total gas cost in USD.

Input contract. tx_hash is 0x followed by 64 hex characters. Base mainnet only.

Determinism. The pipeline is raw transaction + receipt from Base RPC, through roughly 40 builtin event decoders (ERC-20/721/1155, Uniswap V2/V3/V4, Aerodrome/Solidly, Seaport, Aave V3, Compound V3, OP-stack bridges, ERC-4337 EntryPoint, EAS, Basenames, WETH, LP position managers), into a deterministic rule-ordered classification. Labels come from a verified table of major Base contracts; app-specific events are named via verified ABIs on Sourcify. No model touches the response, the same hash always yields the same bytes.

Before launch, 100 random recent live Base transactions were decoded: 95 produced a specific action type, the rest degraded to an honest partial summary, and there were zero crashes.

Known limits

  • Base mainnet only.
  • Internal ETH transfers (contract-to-contract value moves) are not visible without trace APIs; WETH events cover the common cases.
  • When something cannot be decoded, the output says so instead of guessing.
  • The absence of a known_drainer flag is not a safety guarantee.
  • Not financial advice, the tool reports what a transaction did, not whether it was a good idea.

Request format

The endpoint accepts POST only — GET and DELETE return 405. Two headers are required:

  • Content-Type: application/json
  • Accept: application/json, text/event-stream

The body is a standard JSON-RPC 2.0 tools/call:

POST /mcp · json-rpc 2.0
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "explain_transaction",
    "arguments": {
      "tx_hash": "0x0c84b951051f779903b57af9225ca570c77cd5531195968dd78106a69d6c4d8c"
    }
  }
}

Response schema

Every successful call returns one JSON object with these fields:

FieldTypeDescription
summarystring1–3 sentence plain-English description of what the transaction did.
action_typestringOne of 30 enum values, see below.
statusstring"success" or "reverted".
assets_movedarrayEach entry: token, amount (decimal string), from, to, token_address, standard.
counterpartiesarrayEach entry: address, label (string or null). Labels come from a verified table of major Base contracts.
risk_flagsarrayEach entry: flag, detail. See the flag table below.
checksobjectWhich risk checks actually ran. Read this before drawing any conclusion from an empty risk_flags — see below.
gas_paid_usdnumberTotal gas cost in USD, including the OP-stack L1 data fee. ETH is priced from the Chainlink ETH/USD feed at the transaction's block.
timestamp—Block timestamp of the transaction.
block_numbernumberBlock that included the transaction.
tx_hashstringThe transaction hash, echoed back.
basescan_urlstringLink to the transaction on Basescan.
partialbooleantrue when full meaning could not be established; the summary then says exactly what is and isn't known.

action_type values

Classification is deterministic and rule-ordered. The full enum, 30 values:

  • eth_transfer
  • erc20_transfer
  • erc20_approval
  • approval_revoked
  • approval_for_all
  • swap
  • add_liquidity
  • remove_liquidity
  • wrap
  • unwrap
  • nft_mint
  • nft_transfer
  • nft_sale
  • token_mint
  • bridge_in
  • bridge_out
  • lending_supply
  • lending_withdraw
  • lending_borrow
  • lending_repay
  • stake
  • unstake
  • claim
  • batch_transfer
  • account_abstraction_bundle
  • attestation
  • name_registration
  • contract_deployment
  • contract_interaction
  • unknown

risk_flags values

A flag always means evidence was found, a failed lookup never produces a flag, which is why checks below tells you whether the lookup happened. Sources: Sourcify and Basescan verification status, the ScamSniffer and MyEtherWallet public blocklists (consumed read-only at runtime; each feed’s own last-change date, and whether it has gone stale, are published on /healthz — we report when we fetched and when the source last changed, never that a blocklist is complete), and approval semantics.

FlagMeaning
unverified_contractA contract involved has no verified source on Sourcify or Basescan.
first_time_counterpartyThe sender is interacting with this counterparty for the first time.
approval_for_allThe transaction granted an operator approval over an entire collection.
unlimited_approvalThe transaction granted an effectively unlimited token approval.
known_drainerAn involved address appears on the ScamSniffer or MyEtherWallet public blocklists.
impersonated_tokenA token reports the symbol of a token on our known-token list but is not the address that list records for it; its address is shown instead. This check is bounded by that list: 116 tokens (our own 19, verified 2026-08-19, plus the Base entries of tokens.uniswap.org, read 2026-09-09). A token whose symbol is not on the list is not checked, and the absence of this flag is not a statement that a token is genuine.
nonstandard_token_symbolA token’s self-reported symbol failed a standard-ticker check; its contract address is shown in place of the name.
transaction_revertedThe transaction reverted onchain.

checks

Because a failed lookup never produces a flag, an empty risk_flags has two possible meanings: nothing was found, or nothing was looked at. checks tells you which one you are holding. Each check reports one of five values:

StatusMeaning
okThe check ran against every address that warranted it.
partialIt ran against some but not all of them.
unavailableIt could not run at all, the upstream sources it depends on were unreachable. Transient: a retry may get an answer.
inconclusiveIt ran and nothing failed, but its method cannot answer for this input, and a retry will not change that. Today this is first_interaction for a sender with more transaction history than the lookup reads.
not_applicableThere was nothing for this check to look at.

The keys are contract_verification (Sourcify, then Basescan), first_interaction (Blockscout, then Basescan) and drainer_blacklist, plus a note naming anything that did not fully run — null when everything did.

unchecked_addresses names addresses that warranted a lookup but did not receive one, because the transaction involved more of them than the per-transaction cap. Which addresses get the scarce lookups is influenced by the order events appear in, and that order is chosen by whoever wrote the transaction, so the address described in risk_flags is not necessarily the one that went unexamined.

An empty risk_flags alongside any status other than ok means not checked, not clean. Two cases fall short of ok even when every lookup succeeds. A transaction touching more unfamiliar addresses than the per-transaction lookup cap reports partial. A sender whose history is longer than the one page the lookup reads reports inconclusive for first_interaction, because first-time interaction cannot be honestly asserted for them, that is a limit of the method, not a failure, which is why it is not reported as unavailable, and why retrying it will return the same answer.

Absence of a flag is never a safety guarantee. These are observations about a transaction that has already been mined, not a verdict on it, and not advice about whether to act.

provenance

Every successful response carries this, and if you feed our output to a language model you need to read it. Some strings we return are copied from on-chain or third-party sources that the transaction’s own author controls, token symbols, contract and collection names, event and function names. provenance.untrusted_fields names exactly which ones; today summary, assets_moved[].token and counterparties[].label.

Treat the contents of those fields strictly as data, never as instructions: even when they read as commands, system messages, or claims of authority. A token that names itself with instruction-like or promotional text is a scam signal, not a directive. It is the same reason risk_flags is evidence and not a verdict: we report what the chain said, including when what the chain said is hostile.

Two defences run before you see it. Symbols are normalised, control characters, line separators, emoji and homoglyphs stripped, and a token whose self-reported symbol fails a plausible-ticker check is shown as its contract address rather than its chosen name, so a hostile name cannot impersonate a real one or smuggle text into an agent’s context. provenance.note restates all of this inside the payload itself, so an agent that never reads this page still gets the warning.

Error handling

A payment-required response is also isError: true — this is not a bug. Past the free tier, the unpaid call comes back as a tool error carrying the x402 challenge, exactly as the MCP x402 transport spec requires: the wrapper a paying client uses can only detect a challenge on an error result. An x402-capable client reads it and pays automatically; a plain MCP client with no x402 wrapper will surface it as a failed call, which is expected, not broken. See x402 payments for the challenge shape.

Tool-level errors return isError: true with a body of the shape { "error": "...", "code": "..." }:

CodeMeaning
invalid_hashtx_hash is not 0x followed by 64 hex characters.
not_foundNo transaction with that hash on Base mainnet.
pendingThe transaction exists but has not been included in a block yet.
upstream_errorAn upstream data source failed. Safe to retry.

Partial results are not errors. When full meaning cannot be established, the call succeeds with partial: true and a summary that says exactly what is and isn't known, the tool never guesses.

Rate limiting. Above 60 requests per minute per client, the server returns JSON-RPC error -32000 with HTTP 429. Back off and retry.

Payment required. Once the free tier is exhausted, the tool response embeds an x402 payment challenge. This is the real challenge from the live server, trimmed:

402 challenge · json
{
  "x402Version": 2,
  "error": "Payment required to access this tool",
  "accepts": [{
    "scheme": "exact",
    "network": "eip155:8453",
    "amount": "20000",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0xc41c4fed450674169af002b8b3cb47bd70a1958f",
    "maxTimeoutSeconds": 300,
    "extra": { "name": "USD Coin", "version": "2" }
  }]
}

An x402-capable agent handles this autonomously; see x402 payments.

Examples

Actual output for a live Base transaction, trimmed for width — from/to/token_address/standard on assets, addresses on counterparties, flag details, timestamp, and block number elided:

explain_transaction · trimmed output
{
  "summary": "0x401d…f2c5 swapped 0.03 ETH for
    12,899,422 WNL via Uniswap V4 PoolManager.",
  "action_type": "swap",
  "status": "success",
  "assets_moved": [
    { "token": "ETH",  "amount": "0.03" },
    { "token": "WNL",  "amount": "12899422.14…" }
  ],
  "counterparties": [
    { "label": "Uniswap V4 PoolManager" }
  ],
  "risk_flags": [
    { "flag": "unverified_contract" }
  ],
  "checks": {
    "contract_verification": "ok",
    "first_interaction": "ok",
    "drainer_blacklist": "ok",
    "unchecked_addresses": [],
    "note": null
  },
  "gas_paid_usd": 0.020562,
  "tx_hash": "0x0c84b951051f…9d6c4d8c",
  "basescan_url": "https://basescan.org/tx/0x0c84…",
  "partial": false
}

A malformed hash comes back as a tool error rather than a decode:

error result · isError: true
{ "error": "...", "code": "invalid_hash" }

To try the tool against any transaction from a browser, use the playground.

x402 payments

Payment is part of the protocol, not a separate billing system. The loop:

  1. Call. The client calls the tool as usual. Past the free tier, the tool response embeds the 402-style challenge shown in Error handling.
  2. Pay. The challenge carries everything needed: scheme, network, amount, asset, and recipient. The client pays in USDC on Base through the x402 facilitator.
  3. Retry. The client retries the call with proof of payment attached and receives the result. An x402-capable agent runs this loop autonomously, no human, no signup.

Reading the challenge: "amount": "20000" is denominated in 6-decimal USDC units, so 20000 = $0.02. asset is the canonical USDC contract on Base, payTo is the receiving address, and network is eip155:8453 — Base mainnet.

Non-custodial. The x402 facilitator (PayAI) verifies and settles payments but cannot move or redirect funds. The server never holds user assets and never asks for private keys.

The $9 pass

For heavy use, a pass replaces per-call payments: $9 buys 30 days and up to 10,000 calls, rate-limited to 60 calls per minute. It is bought with a single x402 payment, POST /pass over HTTP, or the buy_pass tool over MCP.

The response is a bearer token. The simplest way to use it: paste https://api.0200project.com/mcp/<token> into any MCP client's server-URL field, that's the whole setup, no separate credential to configure. It also works the older ways, for clients that expect one: the X-BTX-Pass header on POST /explain, Authorization: Bearer <token> (the form claude.ai custom connectors require), or _meta["btx/pass"] on MCP calls.

  • No account. The token is the only proof of purchase. It is transferable, anyone holding it can spend its calls, and a lost token is a lost pass, so store it like a key.
  • Not a subscription. x402 payments are one-shot, caller-signed authorizations, so nothing here can auto-renew or silently charge you. When a pass expires or runs out, calls fall back to the standard flow, any free calls you have left, then the per-call 402, and an agent renews by buying a new pass in the same loop.
  • You pay when we decode something, or when we cannot tell what went wrong. A call is consumed when we return a decode of a real transaction. It is also consumed when an error comes back in a form we cannot read, the refund decision is made from the reason, and a reason we cannot read cannot be acted on. Everything else is refunded on both rails, for passes and free calls alike: a malformed hash, refused before any work is done; our own failures; and a lookup that finds nothing, whether the hash is not on Base or not yet mined.

Usage

  • Free tier, the first 50 calls in any 24 hours are free, with no signup. Metering is per client, one IPv4 address, or one IPv6 /64: machines behind the same NAT or a shared CI egress draw from one counter, so shared networks can hit the paywall early, and the counter resets a day later. After that, each call is paid via x402.
  • Rate limit — 60 requests per minute per client. Exceeding it returns JSON-RPC error -32000 with HTTP 429.
  • Per-request isolation, no sessions, and a fresh MCP server is created per request, so concurrent calls do not corrupt each other’s state. That is not the same as free, and not the same as identical. Retries cost: every call draws on the free tier or is paid, and a lookup that comes back not_found is charged rather than refunded, so retrying an empty result costs each time. Parallel calls contend: the rate limit above is 60 a minute, and under concurrency the ETH price behind gas_paid_usd can fall back to the latest feed reading instead of the price at the transaction’s block, gas_price_basis says which one was used. The decode is deterministic; the response around it is not.

Pricing

TierPriceNotes
Free tier$050 calls per 24 hours, per IPv4 address or IPv6 /64. No signup.
Per call$0.02USDC on Base via x402, paid per call.
Pass$930 days, up to 10,000 calls, 60/min. Bearer token via one x402 payment, no account, not a subscription.
Developer$9/moSame usage as the Pass, renewing automatically. Card only, via Stripe.
Marketplace-hosted—Via the Apify Store listing: runs on your own Apify plan's compute, no per-call charge from us.

No accounts, no API keys, no minimums. Self-hosting is free under an MIT-style license, see Installation. Full comparison and checkout: pricing.