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:
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:
{
"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):
{
"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:
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:
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:
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:
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:
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.
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:
| Variable | Purpose |
|---|---|
PAYMENT_MODE | none or x402. Default is free mode. |
X402_PAY_TO | Address that receives x402 payments. |
X402_PRICE_USD | Price per paid call, in USD. |
X402_FACILITATOR_URL | x402 facilitator used to verify and settle payments. |
FREE_CALLS_PER_IP | Free calls granted per client before payment is required. |
BASE_RPC_URLS | Base 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_drainerflag 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/jsonAccept: application/json, text/event-stream
The body is a standard JSON-RPC 2.0 tools/call:
{
"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:
| Field | Type | Description |
|---|---|---|
summary | string | 1–3 sentence plain-English description of what the transaction did. |
action_type | string | One of 30 enum values — see below. |
status | string | "success" or "reverted". |
assets_moved | array | Each entry: token, amount (decimal string), from, to, token_address, standard. |
counterparties | array | Each entry: address, label (string or null). Labels come from a verified table of major Base contracts. |
risk_flags | array | Each entry: flag, detail. See the flag table below. |
gas_paid_usd | number | Total 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_number | number | Block that included the transaction. |
tx_hash | string | The transaction hash, echoed back. |
basescan_url | string | Link to the transaction on Basescan. |
partial | boolean | true 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.
| Flag | Meaning |
|---|---|
unverified_contract | A contract involved has no verified source on Sourcify or Basescan. |
first_time_counterparty | The sender is interacting with this counterparty for the first time. |
approval_for_all | The transaction granted an operator approval over an entire collection. |
unlimited_approval | The transaction granted an effectively unlimited token approval. |
known_drainer | An involved address appears on the ScamSniffer or MyEtherWallet public blocklists. |
transaction_reverted | The transaction reverted onchain. |
Error handling
Tool-level errors return isError: true with a body of the shape { "error": "...", "code": "..." }:
| Code | Meaning |
|---|---|
invalid_hash | tx_hash is not 0x followed by 64 hex characters. |
not_found | No transaction with that hash on Base mainnet. |
pending | The transaction exists but has not been included in a block yet. |
upstream_error | An 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:
{
"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:
{
"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": "...", "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:
- Call. The client calls the tool as usual. Past the free tier, the tool response embeds the 402-style challenge shown in Error handling.
- Pay. The challenge carries everything needed: scheme, network, amount, asset, and recipient. The client pays in USDC on Base through the x402 facilitator.
- 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
-32000with 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
| Tier | Price | Notes |
|---|---|---|
| Free tier | $0 | First 10 calls, metered per client IP. No signup. |
| Per call | $0.02 | USDC on Base via x402, paid per call. |
| 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.