Skip to main content
Glama

aiochainscan

PyPI Python License

aiochainscan reads on-chain history from public block explorers across 85 chains through one async client — and hands it back usable, not raw. Ask for an address's transactions and you get the complete history, not the first page of provider JSON that you then have to stitch, decode and wrap in retries yourself.

async with ChainscanClient.from_config('blockscout', 'ethereum') as client:
    transactions = await client.get_all_transactions(address)   # every page, or an exception

Three things it does for you instead of leaving them as homework:

  • Pagination. get_all_* walks to the end. By default the result is guaranteed complete: every matching record, or an exception — never a silently truncated page.

  • Decoding. ABI decoding of calldata and event logs is built in and needs no extra dependency — no eth-abi, no web3.

  • Failures. Rate limiting, retries, one shape across providers, and optional failover between them.

It runs on free access: 8 chains need no API key at all, BSC works on NodeReal's free tier, and the base install pulls four dependencies.

Status: stable public API (1.x). The public surface is ChainscanClient; provider coverage differs by scanner and endpoint. Released changes are listed in the changelog.

Is this the right tool?

  • Reading history — every transaction, transfer, internal call or log an address ever touched, decoded, without running an indexer or paying for one: this library.

  • Reading live state or sending transactions — contract calls in a hot path, signing, nonces, mempool: use an RPC client such as web3.py. eth_call exists here, but as a convenience on top of an explorer, not as a node client.

  • Arbitrary queries over a whole chain — "every address that did X in 2024": that is an indexer or a warehouse, not an explorer API.

Related MCP server: DuckChain MCP Server

Installation

Python 3.12 or newer is required:

pip install aiochainscan

The base install is dependency-light (httpx, orjson, tenacity, aiolimiter) and needs no extras to decode ABI calldata, checksum addresses, or run the MCP server's default keyless scanner. Extras are installed only when needed:

Extra

Adds

fastabi

Rust accelerator for bulk ABI decoding (separate distribution)

data

Polars DataFrame exports

mcp

MCP server integration

http2

HTTP/2 support; disabled by default

fallback

Pure-Python Keccak fallback

pip install "aiochainscan[data]"

Try it in one command

The shortest end-to-end run — an address's transaction history to CSV, keyless, no checkout:

curl -O https://raw.githubusercontent.com/VaitaR/aiochainscan/main/examples/02_export_to_csv.py
python 02_export_to_csv.py 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --all

The recipe says what it exports, what it does not, and how it behaves when a provider refuses.

Quick start

Blockscout is used without an API key. Its public instances are shared infrastructure: they apply their own rate limiting and may answer a burst of requests with 403 or a bot-protection page. For unattended or high-volume work, configure Etherscan (or a self-hosted Blockscout instance) instead — or put both behind a failover pool.

import asyncio

from aiochainscan import ChainscanClient


async def main() -> None:
    address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'  # vitalik.eth

    async with ChainscanClient.from_config('blockscout_v2', 'ethereum') as client:
        balance = await client.get_balance(address)
        transactions = await client.get_transactions(address)

    print(balance)       # native balance as a base-unit string
    print(transactions)  # one provider page


asyncio.run(main())

Etherscan requires an API key. Pass it explicitly or set ETHERSCAN_KEY:

export ETHERSCAN_KEY='your-api-key'
async with ChainscanClient.from_config('etherscan', 'ethereum') as client:
    block = await client.get_block(20_000_000)

from_config also accepts a base URL instead of a chain name, which points the client at a self-hosted Blockscout or an Etherscan proxy — see the API reference.

Providers

Scanner

Default version

Authentication

Chains

Methods

etherscan

v2

API key

61

33/33

blockscout

v1

None for public instances

32

31/33

blockscout_v2

v2

None for public instances

32

11/33

nodereal

v1

API key (NODEREAL_KEY), free tier

BSC only

25/33

Thirty-three chains need no API key at all — every Blockscout instance (Ethereum, Optimism, Gnosis, Polygon, Base, Arbitrum, Scroll, Sepolia, Mode, Astar, Rootstock, ZKsync Era and twenty more), plus BSC through NodeReal's free tier. Etherscan's 61 are the chains its keyless GET /v2/chainlist registry listed on 2026-09-11; aiochainscan.registry_sync.sync_etherscan_chains() is an opt-in call that re-reads that registry at runtime, so a chain Etherscan adds after a release is constructible without waiting for one.

On Etherscan a free key's reach depends on the endpoint rather than the chain, and BSC has no Blockscout instance at all — the measured details, and the full method list, are in the API reference. aiochainscan scanners prints the same table for your own environment, including which keys are configured.

Complete data, or an exception

Page-returning methods (get_transactions, get_logs, get_token_holders) return one page. get_all_* collects every page and iter_*_streaming yields batches without materializing the result.

Both take guarantee_complete, defaulting to True: every matching record, or an exception. Explorers cap a result window and answer a capped query with a short page indistinguishable from the end of the data — so the library detects the cap and splits the block range until every part fits, instead of handing you a partial history that looks complete.

# Complete, or an exception — the default.
transfers = await client.get_all_token_transfers(address)

# Opt out: fewer requests on wide ranges, truncation possible and silent.
transfers = await client.get_all_token_transfers(address, guarantee_complete=False)

# Constant memory for large histories.
async for batch in client.iter_transactions_streaming(address, batch_size=1_000):
    await store(batch)

When completeness cannot be reached the call raises PaginationDataLossError or CompletenessUnavailableError rather than returning part of the data; the second names the providers that can serve the request whole. Full contract, failover-pool semantics and costs: Pagination, completeness and failover.

One shape across providers

Provider payloads differ field by field (blockNumber vs block_number, nested from objects vs flat strings). The normalized surface returns the same frozen dataclasses whichever provider answered:

txs = await client.get_transactions_normalized(address)
txs[0].hash, txs[0].block_number, txs[0].value_wei   # str, int, int (wei)

get_*_normalized, get_all_*_normalized and iter_*_normalized exist for transactions, token transfers, internal transactions and logs. Everything else stays provider-native.

Every scalar an explorer returns is a string — wei amounts, hex numbers, unix timestamps. Module-level helpers convert them exactly, with no float step:

from aiochainscan import hex_to_int, to_decimal_amount, wei_to_ether

wei_to_ether('1500000000000000000')        # Decimal('1.5') — exact, never float
to_decimal_amount('1500000', decimals=6)   # Decimal('1.5') — USDC-style tokens
hex_to_int('0x1a')                         # 26 — hex string, decimal string or int

Decoding calls and events

Explorers return calldata as an opaque hex blob. Decoding it into a function name and named arguments is part of the base install — abi_pure.py implements the whole ABI spec in pure Python, so nothing beyond the four runtime dependencies is needed. pip install "aiochainscan[fastabi]" swaps in a Rust backend for the same results, faster; it is worth it for bulk work and irrelevant for single decodes.

With a contract address, the ABI is fetched for you:

contract = await client.get_contract(token_address)

async for event in contract.iter_events('Transfer', limit=100):
    print(event.args['from'], event.args['to'], event.args['value'])

With an ABI you already hold, decode directly — no client, no network:

from aiochainscan.decode import decode_transaction_input

decoded = decode_transaction_input(transaction, abi)
decoded['decoded_func']   # 'transfer'
decoded['decoded_data']   # {'to': '0x…', 'amount': 1000000000000}

Two things worth knowing. get_transaction() returns the provider's raw payload — it does not decode on its own. And a type this library cannot decode raises AbiTypeNotSupportedError rather than returning an empty result, so a gap never looks like undecodable calldata. See the SmartContract guide.

ENS

ENS methods are available for Ethereum mainnet. Provider capabilities differ: Blockscout v2 serves reverse lookup from its own address metadata, while forward resolution reads the ENS registry over eth_call and therefore needs a scanner that declares it (etherscan, blockscout v1). A scanner that does not raises MethodNotDeclaredError rather than returning NoneNone means the name (or the reverse record) does not exist.

name = await client.lookup_address(address)
address = await client.resolve_name('vitalik.eth')

See the ENS guide.

For AI agents

An agent that should query chains runs the MCP server — 12 read-only tools over stdio, with an envelope that carries pagination and caveats the agent can act on:

uvx --from "aiochainscan[mcp]" aiochainscan mcp

Setup, the tool table and the response contract: MCP server.

An agent that should write code against the library wants the packaged Agent Skill instead:

npx skills add VaitaR/aiochainscan

The skill (skills/aiochainscan/) carries the rules that decide whether generated code is correct — single-page versus complete history, exact Wei math, provider coverage — plus a provider matrix and recipes. Agents that read Context7 get the same guidance from context7.json without installing anything.

Errors

from aiochainscan import ChainscanRateLimitError, PaginationDataLossError

try:
    transactions = await client.get_all_transactions(address)
except ChainscanRateLimitError:
    raise  # The configured retry policy was exhausted.
except PaginationDataLossError:
    raise  # The provider could not return a complete range safely.

Most exceptions derive from ChainscanClientError, so one clause can bound a call site; MethodNotDeclaredError and AbiTypeNotSupportedError subclass ValueError instead, because both mean the caller asked for something this configuration cannot serve. The full taxonomy is in the API reference.

Documentation

Development

git clone https://github.com/VaitaR/aiochainscan.git
cd aiochainscan
uv sync --extra dev
uv run pytest tests/ -q
uv run mypy aiochainscan --strict
uv run pre-commit run --all-files

See CONTRIBUTING.md for the contribution workflow.

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables comprehensive blockchain data analysis and exploration through the BlockScout API. Provides 56+ tools for transaction analysis, address exploration, token management, smart contract analysis, and market research across multiple blockchain networks.
    -
  • A
    license
    A
    quality
    D
    maintenance
    Description: EVM blockchain intelligence toolkit for AI agents. 20 tools for token prices, gas comparison, swap quotes, yield rates, honeypot detection, and transaction simulation across 5 EVM chains. Zero config, no API keys required.
    26
    30 npm
    3
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI models to interact with Blockscout API for blockchain data, including transactions, blocks, balances, and contract calls. Supports multiple endpoints for comprehensive blockchain exploration.
    4
    -