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-tx-explain v0.1.0, 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-tx-explain.

For agents / machine-readable. The HTTP contract is published as OpenAPI at base-tx-explain.fly.dev/openapi.json, and a plain-text guide for agents lives at base-tx-explain.fly.dev/llms.txt.

Quickstart

The server is one streamable-HTTP endpoint: https://base-tx-explain.fly.dev/mcp. Pick a client — every path calls the same tool.

Claude Code

One command adds the server over streamable HTTP:

terminal
claude mcp add --transport http base-tx-explain https://base-tx-explain.fly.dev/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-tx-explain": {
      "type": "streamable-http",
      "url": "https://base-tx-explain.fly.dev/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-tx-explain": {
      "type": "streamable-http",
      "url": "https://base-tx-explain.fly.dev/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://base-tx-explain.fly.dev/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://base-tx-explain.fly.dev/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://base-tx-explain.fly.dev/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://base-tx-explain.fly.dev/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 IP. The first 10 calls are free with no signup, counted per IP address — machines behind one NAT or a shared CI egress draw from a single counter, which is why the paywall can appear before 10 of your own calls. After that, each call is $0.02 via x402.

Installation

Hosted endpoint

The hosted server at https://base-tx-explain.fly.dev/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-tx-explain.

Self-hosting

The server is open source under an MIT-style license and runs anywhere Docker runs. It is a stateless Express server — no database, no sessions — so it scales horizontally without coordination.

terminal
git clone https://github.com/0200project/base-tx-explain
cd base-tx-explain
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 IP, 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-tx-explain

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.
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.
timestampBlock 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. Sources: Sourcify and Basescan verification status, the ScamSniffer and MyEtherWallet public blocklists (consumed at runtime, refreshed twice daily), 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.
transaction_revertedThe transaction reverted onchain.

Error handling

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": "0xd4ec730ab062f20460727710fce70664948a6bc9",
    "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" }
  ],
  "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.

Usage

  • Free tier — the first 10 calls are free, with no signup. Metering is per client IP: machines behind the same NAT or a shared CI egress draw from one counter, so shared networks can hit the paywall early. 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.
  • Statelessness — the server keeps no sessions and no database, and a fresh MCP server is created per request. Calls are independent and deterministic, so parallelizing and retrying are always safe.

Pricing

TierPriceNotes
Free tier$0First 10 calls, metered per client IP. No signup.
Per call$0.02USDC on Base via x402, paid per call.
Marketplace-hostedVia 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.