Build a Blockchain Transaction Agent

Classify any Ethereum transaction using rule-based logic and Claude AI.

Introduction

30 Second Summary

Every Ethereum transaction tells a story. Someone swapped tokens on a DEX, borrowed from a lending pool, or deployed a new contract. But the raw blockchain data is just hex codes and cryptic addresses.

In this project, you will build a Python CLI agent that takes any Ethereum transaction hash and tells you exactly what happened: the transaction type, which protocol was involved, and a plain-English summary with a confidence score.

What You'll Build

Paste a transaction hash into your terminal and instantly see something like "DEX Swap via Uniswap V3: Swapped 1.5 ETH for 2,847 USDC" with a confidence rating and classification source.

By the end of this project, you'll have:

  • A rule-based classification engine that identifies 80%+ of common transactions (swaps, transfers, approvals) for free using function selector matching and event log parsing.
  • An LLM-powered fallback using Claude that kicks in for complex or unknown transactions, explaining them in natural language with structured reasoning.
  • A modular agent architecture with swappable components (data fetcher, decoder, classifier) designed as the foundation for a larger autonomous agent system.
  • Secret Mission: Add batch processing mode that accepts a wallet address and classifies its last N transactions, producing a portfolio activity summary.

Are there any prerequisites?

This project assumes you're comfortable with Python and familiar with Ethereum transactions and smart contracts (what a transaction hash is, what gas means, what a smart contract does). You don't need to be a Solidity developer.

Before We Start

To decode blockchain transactions, you need two things: access to the chain data itself and an AI model that can reason about ambiguous cases.

In this step, you'll set up a Python environment with web3.py for blockchain data access and the Anthropic SDK for Claude-powered reasoning. Once everything is wired up, you'll confirm that your agent can reach the Ethereum network and talk to Claude.

In this step, get ready to:
  • Set up your project directory and Python virtual environment.
  • Install blockchain and AI dependencies.
  • Verify your Ethereum RPC connection and Anthropic API access.
Create your project directory and virtual environment

Every Python project starts with an isolated environment so dependencies don't conflict with your system packages.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir tx-identifier && cd tx-identifier
  • Create a Python virtual environment and activate it by running:
python3 -m venv .venv && source .venv/bin/activate

Your terminal prompt now shows (.venv) at the beginning, confirming the virtual environment is active.

  • Install the project dependencies by running:
pip install web3==7.16.0 anthropic==0.117.0 requests==2.32.3

You'll see download progress for each package and its sub-dependencies. The final line reads Successfully installed followed by the full list of packages.

What are these packages?

  • web3.py connects your Python code to the Ethereum blockchain via RPC calls.
  • anthropic is the official SDK for calling Claude models.
  • requests handles HTTP calls to external APIs like 4byte.directory.

Seeing an installation error?

Make sure your virtual environment is active (you see (.venv) in your prompt). If you see a permissions error, do not use sudo. Deactivate and recreate the venv instead.

help me fix my pip install error

Set your environment variables

Your agent needs two pieces of configuration: an API key for Claude and an RPC endpoint for Ethereum.

  • Set your Anthropic API key by running this command (replace the placeholder with your real key):
export ANTHROPIC_API_KEY="your-api-key-here"
  • Set the Ethereum RPC URL by running:
export ETH_RPC_URL="https://ethereum-rpc.publicnode.com"

Why PublicNode?

PublicNode provides a free Ethereum RPC endpoint with no signup or API key required. It's perfect for development and testing without adding another account to manage.

  • Confirm the RPC URL is set by running:
echo $ETH_RPC_URL

You should see https://ethereum-rpc.publicnode.com printed.

Verify your setup

Time to confirm that both connections work before building the rest of the agent.

  • Create a file called verify.py in your tx-identifier folder by running:
touch verify.py
  • Open verify.py in your text editor and paste the following code:
import os
import sys
from web3 import Web3
import anthropic


def main():
    rpc_url = os.environ.get("ETH_RPC_URL", "https://ethereum-rpc.publicnode.com")
    w3 = Web3(Web3.HTTPProvider(rpc_url))

    if not w3.is_connected():
        print("ERROR: Cannot connect to Ethereum RPC")
        sys.exit(1)

    block = w3.eth.block_number
    print(f"Connected to Ethereum. Latest block: {block}")

    client = anthropic.Anthropic()
    print("Anthropic client ready")


if __name__ == "__main__":
    main()

What does this code do?

  • It reads your ETH_RPC_URL environment variable and creates a Web3 connection to Ethereum.
  • It checks the connection is alive, then fetches the latest block number as proof.
  • It initializes the Anthropic client (which automatically reads your ANTHROPIC_API_KEY from the environment).
  • Save verify.py.

Before you run this, what do you expect to see if both connections succeed?

  • Run your verification script:
python verify.py

You should see two lines: a block number confirming your Ethereum connection, and a message confirming the Anthropic client initialized.

Connected to Ethereum. Latest block: 22543817
Anthropic client ready

What's a block number?

Every Ethereum block is sequentially numbered. Seeing a recent block number (in the millions) confirms your RPC call reached the live network and got a response.

Seeing an RPC connection error?

Check that you exported ETH_RPC_URL in the same terminal session where you're running the script. If you opened a new terminal, you need to re-export both variables and reactivate your venv.

If you see an Anthropic authentication error, double-check that your API key is correct and has no extra spaces.

help me fix my verify.py connection error

✔️ Awesome, I've got everything!

Great. Double check you've saved verify.py and you can see both success messages in your terminal.

ⓧ I'd like to double check the full code

Here is the complete verify.py file:

import os
import sys
from web3 import Web3
import anthropic


def main():
    rpc_url = os.environ.get("ETH_RPC_URL", "https://ethereum-rpc.publicnode.com")
    w3 = Web3(Web3.HTTPProvider(rpc_url))

    if not w3.is_connected():
        print("ERROR: Cannot connect to Ethereum RPC")
        sys.exit(1)

    block = w3.eth.block_number
    print(f"Connected to Ethereum. Latest block: {block}")

    client = anthropic.Anthropic()
    print("Anthropic client ready")


if __name__ == "__main__":
    main()

Your environment is ready. You have a live connection to Ethereum and a working AI client. Next up, you'll build the data fetcher that pulls raw transaction details from the blockchain.

Fetch and Decode Raw Transaction Data

Your environment is connected to Ethereum and Claude is ready. Now you need the raw data that your classifier will work with.

Every Ethereum transaction carries structured information: who sent it, who received it, how much ETH moved, and most importantly, the input data that tells you what function was called. In this step, you will build a data fetcher module that pulls all of this from the blockchain and decodes it into something your agent can reason about.

In this step, get ready to:
  • Build a fetcher module that retrieves transaction data from the Ethereum RPC.
  • Parse ERC-20 Transfer event logs to identify token movements.
  • Resolve function selectors using the 4byte.directory API to decode what a transaction did.
Build the data fetcher module

Your fetcher module needs a connection to the Ethereum RPC and some constants to identify known event types. This foundation gives every other function in the file a shared way to talk to the blockchain.

  • Create a new file called fetcher.py in your tx-identifier folder with the following code:
import os
import requests
from web3 import Web3

# The keccak256 hash of Transfer(address,address,uint256)
# Used to identify ERC-20 token transfer events in logs
ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

# Base URL for the 4byte.directory function signature API
FOUR_BYTE_API = "https://www.4byte.directory/api/v1/signatures/"


def get_web3():
    # Connect to the Ethereum RPC using the env var or the free PublicNode default
    rpc_url = os.environ.get("ETH_RPC_URL", "https://ethereum-rpc.publicnode.com")
    return Web3(Web3.HTTPProvider(rpc_url))

What does this code do?

ERC20_TRANSFER_TOPIC is the keccak256 hash of the Transfer(address,address,uint256) event signature defined in the ERC-20 standard. Every token transfer on Ethereum emits a log with this exact topic, so you can use it as a fingerprint to find token movements inside any transaction.

get_web3() creates a reusable connection to the Ethereum node. Every function in this module will call it rather than managing its own connection.

  • Save fetcher.py and verify the module imports cleanly by running:
python -c "from fetcher import get_web3; print(get_web3().eth.block_number)"

You should see a large number printed (the latest Ethereum block number). This confirms your module connects to the RPC successfully.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is still activated. You should see (.venv) at the start of your terminal prompt.
  • Confirm you are inside the tx-identifier folder where fetcher.py lives.

Help me debug a ModuleNotFoundError when importing fetcher.py

Parse event logs

When a smart contract transfers tokens, it emits a Transfer event log. The raw log contains the sender, receiver, and amount encoded in its topics and data fields. Your parser needs to extract these into a readable format.

  • Add the parse_erc20_transfers function below get_web3() in fetcher.py:
def parse_erc20_transfers(logs):
    transfers = []
    for log in logs:
        topics = log.get("topics", [])
        # Check if this log is a Transfer event by matching topic0
        if len(topics) >= 3 and topics[0].hex() == ERC20_TRANSFER_TOPIC:
            # Sender and receiver are encoded in topics 1 and 2 (last 40 hex chars = address)
            from_addr = "0x" + topics[1].hex()[-40:]
            to_addr = "0x" + topics[2].hex()[-40:]
            # The amount is stored in the log data field
            raw_amount = int(log["data"].hex(), 16) if log["data"] else 0
            transfers.append({
                "token_address": log["address"],
                "from": from_addr,
                "to": to_addr,
                "raw_amount": raw_amount,
            })
    return transfers

What does this code do?

  • It iterates through every log in a transaction receipt and checks if the first topic matches the ERC20_TRANSFER_TOPIC constant.
  • Topics 1 and 2 contain the sender and receiver addresses, padded to 32 bytes. Slicing the last 40 hex characters extracts just the 20-byte address.
  • The data field holds the transfer amount as a raw integer. This is the amount in the token's smallest unit (like wei for ETH).
  • Save fetcher.py.
Extract the function selector

The first 4 bytes of a transaction's input data are its function selector. This is a fingerprint that identifies which smart contract function was called. The 4byte.directory API maintains a public database that maps these selectors back to human-readable function names.

  • Add the resolve_selector function below get_web3() and above parse_erc20_transfers in fetcher.py:
def resolve_selector(selector_hex):
    # Query the 4byte.directory API to look up what function this selector represents
    try:
        resp = requests.get(
            FOUR_BYTE_API,
            params={"hex_signature": selector_hex},
            timeout=5,
        )
        if resp.status_code == 200:
            results = resp.json().get("results", [])
            if results:
                # Return the first matching function signature
                return results[0]["text_signature"]
    except Exception:
        pass
    return None

What does this code do?

It sends a GET request to https://www.4byte.directory/api/v1/signatures/ with the selector as a query parameter. The API returns matching function signatures. For example, querying 0xa9059cbb returns transfer(address,uint256).

The function wraps everything in a try/except so that if the API is down, your agent gracefully returns None instead of crashing.

  • Save fetcher.py and verify the selector resolver works by running:
python -c "from fetcher import resolve_selector; print(resolve_selector('0xa9059cbb'))"

You should see transfer(address,uint256) printed. This confirms your code successfully queried 4byte.directory and decoded the ERC-20 transfer function selector.

Seeing None instead of a function name?

  • Check that requests is installed. Run pip list and confirm you see requests in the output.
  • The 4byte.directory API may be temporarily unreachable. Wait a moment and try again.

Help me debug why resolve_selector returns None

Now you have the two helper functions ready. Time to wire them into the main fetch function that pulls everything together.

  • Add the fetch_transaction function at the bottom of fetcher.py, below parse_erc20_transfers:
def fetch_transaction(tx_hash):
    w3 = get_web3()
    # Fetch the transaction object and its receipt from the blockchain
    tx = w3.eth.get_transaction(tx_hash)
    receipt = w3.eth.get_transaction_receipt(tx_hash)

    # Extract the function selector (first 4 bytes of input data)
    input_data = tx["input"].hex() if tx["input"] else ""
    selector = input_data[:10] if len(input_data) >= 10 else None
    function_name = resolve_selector(selector) if selector else None

    # Parse any ERC-20 transfer events from the receipt logs
    transfers = parse_erc20_transfers(receipt["logs"])

    return {
        "hash": tx_hash,
        "from_address": tx["from"],
        "to_address": tx.get("to"),
        "value_wei": tx["value"],
        "value_eth": float(Web3.from_wei(tx["value"], "ether")),
        "input_data": input_data,
        "selector": selector,
        "function_name": function_name,
        "status": receipt["status"],
        "gas_used": receipt["gasUsed"],
        "transfers": transfers,
        "logs": receipt["logs"],
    }

What does this code do?

  • get_transaction fetches the raw transaction (sender, receiver, value, input data). get_transaction_receipt fetches the result of executing the transaction (status, gas used, event logs).
  • The selector is extracted as the first 10 characters of input_data (0x prefix + 4 bytes = 10 hex chars). This is passed to resolve_selector to get a human-readable function name.
  • The function returns a flat dictionary combining everything your classifier will need: addresses, values, decoded function name, token transfers, and raw logs.
  • Finally, add the CLI entry point at the very bottom of fetcher.py so you can run it directly:
if __name__ == "__main__":
    import sys
    import json

    if len(sys.argv) < 2:
        print("Usage: python fetcher.py <tx_hash>")
        sys.exit(1)

    result = fetch_transaction(sys.argv[1])
    print(json.dumps(result, indent=2, default=str))

What does this code do?

The if __name__ == "__main__" block makes fetcher.py runnable as a standalone script. It takes a transaction hash from the command line, fetches the data, and prints the result as formatted JSON. The default=str argument handles objects that JSON can't serialize natively (like large integers and hex bytes).

  • Save fetcher.py.

Before you run the fetcher, what do you expect to see when you pass it a real transaction hash? Think about which fields will be populated for a simple ETH transfer versus a smart contract interaction.

  • Test your fetcher with a real Ethereum transaction. Go to etherscan.io, click on any recent transaction, and copy its hash (the long 0x... value at the top of the page).
  • Run the fetcher with your chosen transaction hash:
python fetcher.py [[TX_HASH="0x followed by 64 hex characters from etherscan.io"]]

You should see structured JSON output with from_address, to_address, value_eth, input_data, and function_name fields. If you picked a token transfer or DEX swap, the transfers array will contain decoded ERC-20 movements. If you picked a simple ETH send, transfers will be empty and function_name will be null.

Getting a transaction not found error?

  • Make sure your hash starts with 0x and is exactly 66 characters long (0x + 64 hex characters).
  • The PublicNode RPC only serves mainnet Ethereum transactions. If you copied a hash from a testnet (Sepolia, Goerli), it will not be found.
  • Very recent transactions (under a minute old) may not be indexed yet. Try a transaction from a few blocks back.

Help me debug why my transaction fetch is failing

✔️ Awesome, I've got everything!

Great. Double check you have saved fetcher.py and that your JSON output includes all the expected fields.

ⓧ I'd like to double check the full code

Your fetcher.py should contain these sections in order. Scroll up and confirm each one is present:

  • Imports (os, requests, Web3).
  • Constants ERC20_TRANSFER_TOPIC and FOUR_BYTE_API.
  • get_web3() function.
  • resolve_selector(selector_hex) function.
  • parse_erc20_transfers(logs) function.
  • fetch_transaction(tx_hash) function.
  • if __name__ == "__main__" block with sys.argv and json.dumps output.

Your data layer is complete. You can now pull any Ethereum transaction and get back a structured breakdown of who did what. Next up, you will build the rule-based classifier that turns this raw data into an instant, free classification for common transaction types.

Build the Rule-Based Classifier

Your data fetcher can pull raw transaction details from the blockchain. Now you need something that can look at that data and decide what type of transaction it is.

Most Ethereum transactions follow predictable patterns. An ERC-20 transfer always uses the same function selector. A Uniswap swap always hits the same router address. A rule-based classifier exploits these patterns to classify transactions instantly at zero cost. This is the fast path that production blockchain analysis tools rely on before ever reaching for an LLM.

In this step, get ready to:
  • Define a classification taxonomy with known function selectors and protocol addresses.
  • Implement a rule-based classification function that matches transaction patterns deterministically.
  • Wire the classifier into a CLI that takes a transaction hash and prints a structured result.
Define the classification taxonomy

Every smart contract function has a unique 4-byte selector at the start of its call data. By mapping known selectors to transaction types, you can classify common operations without any AI involved.

  • Create a new file called classifier.py in your tx-identifier folder and add the transaction types list and swap selectors by pasting the following code:
# All possible transaction categories
TRANSACTION_TYPES = [
    "TRANSFER",
    "SWAP",
    "APPROVAL",
    "LENDING",
    "NFT_TRADE",
    "CONTRACT_DEPLOY",
    "UNKNOWN",
]

# Known DEX swap function selectors
SWAP_SELECTORS = {
    "0x38ed1739": "swapExactTokensForTokens",
    "0x7ff36ab5": "swapExactETHForTokens",
    "0x18cbafe5": "swapExactTokensForETH",
    "0x5c11d795": "swapExactTokensForTokensSupportingFeeOnTransferTokens",
    "0xb6f9de95": "swapExactETHForTokensSupportingFeeOnTransferTokens",
    "0x791ac947": "swapExactTokensForETHSupportingFeeOnTransferTokens",
    "0x04e45aaf": "exactInputSingle",
    "0xb858183f": "exactInput",
    "0x5023b4df": "exactOutputSingle",
    "0x09b81346": "exactOutput",
    "0x3593564c": "execute",
}

What does this code do?

TRANSACTION_TYPES defines the seven categories your agent can assign to a transaction. These cover the most common on-chain activities.

SWAP_SELECTORS maps the first 4 bytes of a swap transaction's call data to its human-readable function name. These selectors cover Uniswap V2, V3, and the Universal Router.

  • Below SWAP_SELECTORS, add the remaining selector dictionaries and the protocol address mapping:
# Token approval selectors
APPROVAL_SELECTORS = {
    "0x095ea7b3": "approve",
    "0xa22cb465": "setApprovalForAll",
}

# Direct token transfer selectors
TRANSFER_SELECTORS = {
    "0xa9059cbb": "transfer",
    "0x23b872dd": "transferFrom",
}

# DeFi lending protocol selectors
LENDING_SELECTORS = {
    "0xe8eda9df": "deposit",
    "0x69328dec": "withdraw",
    "0xa415bcad": "borrow",
    "0x573ade81": "repay",
    "0x02c205f0": "liquidationCall",
    "0x617ba037": "supply",
}

# Map well-known contract addresses to protocol names
KNOWN_PROTOCOLS = {
    "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": "Uniswap V2 Router",
    "0xe592427a0aece92de3edee1f18e0157c05861564": "Uniswap V3 Router",
    "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad": "Uniswap Universal Router",
    "0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2": "Aave V3 Pool",
    "0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9": "Aave V2 Pool",
    "0x00000000006c3852cbef3e08e8df289169ede581": "OpenSea Seaport",
}

What are these selectors and addresses?

Each dictionary maps a known 4-byte function selector (like 0x095ea7b3 for approve) to its human-readable name. These selectors are the same across all ERC-20 tokens because they implement the same interface.

KNOWN_PROTOCOLS maps contract addresses to protocol names. When a transaction interacts with one of these addresses, you can immediately identify which protocol is involved.

  • Save classifier.py.
  • Verify the module loads correctly by running:
python -c "from classifier import KNOWN_PROTOCOLS; print(len(KNOWN_PROTOCOLS), 'protocols mapped')"

You should see 6 protocols mapped printed in your terminal.

Seeing an ImportError?

Make sure you are running the command from inside the tx-identifier folder where classifier.py lives. Also check for any typos in the dictionary syntax (missing commas or colons).

help me debug an ImportError when importing classifier.py

Implement the classification rules

With the selector dictionaries in place, you can now write a function that checks each transaction against these patterns in priority order. The first matching rule wins.

  • At the bottom of classifier.py, add the first part of the classification function that handles contract deployments and plain ETH transfers:
def classify_rule_based(tx_data):
    # Extract key fields from the transaction data
    selector = tx_data.get("selector")
    to_address = tx_data.get("to_address")
    value_eth = tx_data.get("value_eth", 0)
    input_data = tx_data.get("input_data", "")
    transfers = tx_data.get("transfers", [])

    # Contract deployment: no recipient address
    if to_address is None:
        return {
            "type": "CONTRACT_DEPLOY",
            "protocol": None,
            "summary": "Deployed a new smart contract",
            "confidence": "High",
            "source": "RULE",
        }

    # Plain ETH transfer: no input data, positive value
    if (not input_data or input_data == "0x") and value_eth > 0:
        return {
            "type": "TRANSFER",
            "protocol": None,
            "summary": f"Sent {value_eth:.4f} ETH",
            "confidence": "High",
            "source": "RULE",
        }

    # Look up protocol by contract address
    protocol = KNOWN_PROTOCOLS.get(to_address.lower() if to_address else "")

What does this code do?

The function extracts the key fields it needs from the transaction dict that fetch_transaction() returns. It then checks two easy-to-identify patterns first.

If to_address is None, the transaction created a new contract. If there is no call data but ETH was sent, it is a plain value transfer. Finally, it looks up whether the destination address belongs to a known protocol.

  • Directly below the protocol = KNOWN_PROTOCOLS.get(...) line you just added, paste the approval and swap classification rules:
    # Token approval check
    if selector in APPROVAL_SELECTORS:
        return {
            "type": "APPROVAL",
            "protocol": protocol,
            "summary": f"Approved token spending ({APPROVAL_SELECTORS[selector]})",
            "confidence": "High",
            "source": "RULE",
        }

    # DEX swap check
    if selector in SWAP_SELECTORS:
        summary = f"Token swap ({SWAP_SELECTORS[selector]})"
        if len(transfers) >= 2:
            summary = f"Swapped tokens via {protocol or 'DEX'}"
        return {
            "type": "SWAP",
            "protocol": protocol,
            "summary": summary,
            "confidence": "High",
            "source": "RULE",
        }

What does this code do?

These rules check the function selector against the known approval and swap dictionaries. If the selector matches 0x095ea7b3 (approve), it immediately returns an APPROVAL classification.

For swaps, it also checks whether two or more token Transfer events appeared in the logs. Multiple token movements confirm the swap actually executed (not just that the function was called).

  • Save classifier.py and confirm the function loads by running:
python -c "from classifier import classify_rule_based; print('Classifier loaded')"

You should see Classifier loaded printed. The function is syntactically valid even though you have not finished all the rules yet.

  • Return to classifier.py and add the remaining rules directly below the swap check. Paste this code at the same indentation level, continuing inside the function:
    # ERC-20 transfer check
    if selector in TRANSFER_SELECTORS:
        return {
            "type": "TRANSFER",
            "protocol": protocol,
            "summary": f"Token transfer ({TRANSFER_SELECTORS[selector]})",
            "confidence": "High",
            "source": "RULE",
        }

    # Lending protocol check
    if selector in LENDING_SELECTORS:
        return {
            "type": "LENDING",
            "protocol": protocol,
            "summary": f"Lending action ({LENDING_SELECTORS[selector]})",
            "confidence": "High",
            "source": "RULE",
        }
  • Finally, add the heuristic fallback and the UNKNOWN catch-all directly below the lending check:
    # Heuristic: multiple transfers on a known protocol likely means swap
    if len(transfers) >= 2 and protocol:
        return {
            "type": "SWAP",
            "protocol": protocol,
            "summary": f"Likely swap via {protocol} (multiple token transfers detected)",
            "confidence": "Medium",
            "source": "RULE",
        }

    # No rule matched
    return {
        "type": "UNKNOWN",
        "protocol": protocol,
        "summary": None,
        "confidence": None,
        "source": "RULE",
    }

What do these final rules cover?

The transfer and lending checks work the same way as the approval check. They match the selector against their respective dictionaries.

The heuristic rule is a soft match. If no selector matched but the transaction hit a known protocol and moved multiple tokens, it is probably a swap. This gets a "Medium" confidence because it is inferred, not deterministic.

If nothing matches at all, the function returns UNKNOWN. This is the signal that tells the LLM fallback (coming in the next step) to take over.

  • Save classifier.py and test the CONTRACT_DEPLOY rule with a quick inline check:
python -c "from classifier import classify_rule_based; r = classify_rule_based({'to_address': None, 'selector': None, 'value_eth': 0, 'input_data': '', 'transfers': []}); print(r['type'], '-', r['summary'])"

You should see CONTRACT_DEPLOY - Deployed a new smart contract printed.

Seeing an IndentationError?

All the rule blocks must be indented at the same level inside the classify_rule_based function (4 spaces). Check that you did not accidentally paste code outside the function body or mix tabs with spaces.

help me fix an IndentationError in my classify_rule_based function

Wire it together into a CLI

The classification logic is ready. Now you need a simple command-line entry point that fetches a transaction and runs it through the classifier.

  • Create a new file called classify.py in the same folder and add the following code:
import sys
import os
import json
from fetcher import fetch_transaction
from classifier import classify_rule_based


def main():
    # Parse the transaction hash from command line arguments
    args = sys.argv[1:]
    if not args:
        print("Usage: python classify.py <tx_hash>")
        sys.exit(0)
    tx_hash = args[0]
    # Fetch raw data and classify
    tx_data = fetch_transaction(tx_hash)
    result = classify_rule_based(tx_data)
    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()

What does this code do?

This is the CLI glue. It takes a transaction hash from the command line, passes it to fetch_transaction() to pull the raw blockchain data, then feeds that data into classify_rule_based() to determine the transaction type.

The result prints as formatted JSON so you can clearly see the classification type, protocol, summary, confidence, and source.

  • Save classify.py.

Before you run it with a real transaction, what do you think the classifier will return for a token approval transaction? Will it say APPROVAL with high confidence, or will it fall through to UNKNOWN?

  • Test the classifier with a known token approval transaction by running:
python classify.py 0xf4a397a64c8090aad23dcb1f8f26fe8f7b502e35c727134dfad166bc771563b0

You should see JSON output with "type": "APPROVAL", "confidence": "High", and "source": "RULE". The classifier matched the 0x095ea7b3 selector deterministically.

Why is this powerful?

That classification happened instantly with zero API calls. No LLM tokens, no network latency to an AI service. For the 80%+ of transactions that match known patterns, this rule-based path is all you need.

Getting a connection error or timeout?

The public RPC endpoint may occasionally be slow. Wait a few seconds and try again. Make sure your ETH_RPC_URL environment variable is still set in your terminal session.

help me troubleshoot RPC connection issues when running classify.py

✔️ Awesome, I've got everything!

Great. Double check you have saved both classifier.py and classify.py.

ⓧ I'd like to double check the full code

Your classifier.py should contain these sections in order. Scroll up and confirm each is present:

  • TRANSACTION_TYPES list (7 items).
  • SWAP_SELECTORS dict (11 entries).
  • APPROVAL_SELECTORS dict (2 entries).
  • TRANSFER_SELECTORS dict (2 entries).
  • LENDING_SELECTORS dict (6 entries).
  • KNOWN_PROTOCOLS dict (6 entries).
  • classify_rule_based(tx_data) function with rules in order: CONTRACT_DEPLOY, ETH TRANSFER, APPROVAL, SWAP, TRANSFER, LENDING, heuristic swap, UNKNOWN.

Your classify.py should contain:

import sys
import os
import json
from fetcher import fetch_transaction
from classifier import classify_rule_based


def main():
    args = sys.argv[1:]
    if not args:
        print("Usage: python classify.py <tx_hash>")
        sys.exit(0)
    tx_hash = args[0]
    tx_data = fetch_transaction(tx_hash)
    result = classify_rule_based(tx_data)
    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()

Your rule-based classifier is live. It can identify approvals, swaps, transfers, lending actions, and contract deployments with zero AI cost. Next up, you will add the Claude LLM fallback that kicks in for the transactions your rules cannot handle.

Add the Claude LLM Fallback

Your rule-based classifier handles common patterns deterministically. Transfers, swaps, approvals, and lending actions all get classified instantly with zero API cost.

But Ethereum has thousands of protocols and function signatures your rules have never seen. When the classifier returns UNKNOWN, you need an AI reasoning layer that can look at the raw transaction context and figure out what happened. That's where Claude steps in as a fallback.

In this step, get ready to:
  • Experience the rule-based classifier hitting its limit on a complex transaction.
  • Build an LLM classifier module that sends transaction context to Claude.
  • Integrate the LLM fallback into your classification pipeline.
See the classifier hit its limit

Most transactions on Ethereum mainnet come from protocols your rule-based classifier doesn't know about. Governance votes, yield farming deposits, NFT mints from new collections, bridge transfers. Your known selectors cover a tiny slice of on-chain activity.

  • Run your classifier against a transaction that interacts with a protocol not in your KNOWN_PROTOCOLS dictionary or uses a function selector not in your lookup tables. Any recent complex DeFi transaction will do.
  • Before you run it, predict: what do you expect the output to look like for a transaction your rules have never seen?
  • Run the classifier by replacing the hash below with the one you found:
python classify.py [[TX_HASH="a transaction hash from a protocol not in your known list"]]

You see output like this:

{
  "type": "UNKNOWN",
  "protocol": null,
  "summary": null,
  "confidence": null,
  "source": "RULE"
}

That's the gap. Your classifier throws up its hands on anything it hasn't been explicitly taught to recognize. This is exactly the problem the LLM fallback will solve.

Why rule-based first, LLM second?

This mirrors how production blockchain analysis tools work. The rule-based engine handles 80%+ of common transactions instantly and for free. The LLM only gets called for the remaining ambiguous cases, keeping costs low and latency down.

Each LLM classification costs fractions of a cent with claude-haiku-4-5-20251001. But calling it for every single transaction would add unnecessary latency and cost at scale.

Build the LLM classifier module

The LLM classifier needs three things: a system prompt that constrains Claude to your taxonomy, a function that formats transaction data into a readable prompt, and a function that sends the request and parses the structured response.

  • Create a new file called llm_classifier.py in your tx-identifier folder and add the imports and system prompt:
import json
import os
import anthropic

SYSTEM_PROMPT = """You are a blockchain transaction analyst. Given transaction data from the Ethereum blockchain, classify the transaction and explain what happened.

You MUST respond with valid JSON in this exact format:
{
  "type": "TRANSFER|SWAP|APPROVAL|LENDING|NFT_TRADE|CONTRACT_DEPLOY|UNKNOWN",
  "protocol": "protocol name or null",
  "summary": "one-line plain English explanation of what happened",
  "confidence": "High|Medium|Low",
  "reasoning": "brief explanation of how you determined this"
}

Transaction types:
- TRANSFER: Simple ETH or token send from one address to another
- SWAP: Token exchange on a DEX (Uniswap, SushiSwap, Curve, etc.)
- APPROVAL: Granting a contract permission to spend tokens
- LENDING: Supply, borrow, repay, or withdraw on a lending protocol (Aave, Compound, etc.)
- NFT_TRADE: Buying, selling, or transferring NFTs
- CONTRACT_DEPLOY: Creating a new smart contract
- UNKNOWN: Cannot determine with confidence

Be concise. Focus on what the user DID, not the technical implementation."""

What does this code do?

The SYSTEM_PROMPT constrains Claude to your exact taxonomy. It must respond with the same transaction types your rule-based classifier uses (TRANSFER, SWAP, APPROVAL, etc.), which keeps the output consistent regardless of which path classified the transaction.

Requesting JSON output with specific fields means you can parse the response programmatically and slot it into the same result structure your pipeline already expects.

  • Add the build_context function below the SYSTEM_PROMPT constant. This formats the raw transaction data into a readable string for Claude:
def build_context(tx_data):
    lines = [
        f"From: {tx_data['from_address']}",
        f"To: {tx_data['to_address']}",
        f"Value: {tx_data['value_eth']} ETH",
        f"Status: {'Success' if tx_data['status'] == 1 else 'Failed'}",
        f"Gas Used: {tx_data['gas_used']}",
    ]

    if tx_data.get("function_name"):
        lines.append(f"Function Called: {tx_data['function_name']}")
    elif tx_data.get("selector"):
        lines.append(f"Function Selector: {tx_data['selector']}")

    if tx_data.get("transfers"):
        lines.append(f"\nToken Transfers ({len(tx_data['transfers'])}):")
        for t in tx_data["transfers"][:10]:
            lines.append(
                f"  {t['from'][:10]}... -> {t['to'][:10]}... "
                f"| Token: {t['token_address'][:10]}... "
                f"| Amount: {t['raw_amount']}"
            )

    if tx_data.get("input_data") and len(tx_data["input_data"]) > 10:
        truncated = tx_data["input_data"][:210]
        lines.append(f"\nInput Data (first 100 bytes): {truncated}")

    return "\n".join(lines)

What does this code do?

  • Assembles the transaction's key fields (from, to, value, status, gas) into a readable block Claude can reason about.
  • Includes the resolved function name if 4byte.directory found one, otherwise falls back to the raw selector hex.
  • Lists up to 10 token transfers so Claude can see the flow of tokens between addresses.
  • Truncates input data to avoid sending excessively long calldata that would waste tokens.
  • Save llm_classifier.py and confirm the module loads by running:
python -c "from llm_classifier import build_context; print('Module loads OK')"

You should see Module loads OK printed. This confirms your file is syntactically valid and the imports resolve.

Seeing a SyntaxError?

The triple-quoted SYSTEM_PROMPT string must open and close with exactly three double quotes. Check that none were accidentally deleted.

help me debug a SyntaxError in my llm_classifier.py file

  • Now add the main classify_with_llm function below build_context. This sends the transaction context to Claude and parses the JSON response:
def classify_with_llm(tx_data):
    client = anthropic.Anthropic()

    context = build_context(tx_data)

    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=512,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Classify this Ethereum transaction:\n\n{context}",
            }
        ],
    )

    response_text = message.content[0].text

    try:
        start = response_text.find("{")
        end = response_text.rfind("}") + 1
        result = json.loads(response_text[start:end])
        result["source"] = "LLM"
        result["tokens_used"] = message.usage.input_tokens + message.usage.output_tokens
        return result
    except (json.JSONDecodeError, ValueError):
        return {
            "type": "UNKNOWN",
            "protocol": None,
            "summary": response_text[:200],
            "confidence": "Low",
            "reasoning": "Failed to parse structured response",
            "source": "LLM",
            "tokens_used": message.usage.input_tokens + message.usage.output_tokens,
        }

What does this code do?

  • Creates an Anthropic client that automatically reads your ANTHROPIC_API_KEY environment variable.
  • Calls claude-haiku-4-5-20251001 which is optimized for structured classification tasks at minimal cost.
  • Extracts the JSON object from Claude's response using bracket-finding. This handles cases where Claude adds text before or after the JSON.
  • Falls back gracefully if Claude returns malformed JSON. The agent still provides whatever text Claude gave, just with low confidence.
  • Save llm_classifier.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before continuing.

ⓧ I'd like to double check the full code

Scroll up and confirm your llm_classifier.py contains these three sections in order:

  • Imports (json, os, anthropic) and the SYSTEM_PROMPT constant.
  • The build_context(tx_data) function that assembles transaction fields into a prompt string.
  • The classify_with_llm(tx_data) function that calls Claude and parses the JSON response.
Integrate into the classification pipeline

Now you need to wire the LLM classifier into your existing pipeline so it kicks in automatically when the rule-based engine returns UNKNOWN.

  • Open classify.py and replace its entire contents with the updated version that includes the LLM fallback and a --force-llm flag:
import sys
import os
import json
from fetcher import fetch_transaction
from classifier import classify_rule_based
from llm_classifier import classify_with_llm


def main():
    args = sys.argv[1:]
    if not args or args[0] in ("-h", "--help"):
        print("Usage: python classify.py <tx_hash> [--force-llm]")
        sys.exit(0)

    tx_hash = args[0]
    force_llm = "--force-llm" in args

    tx_data = fetch_transaction(tx_hash)

    if force_llm:
        result = classify_with_llm(tx_data)
    else:
        result = classify_rule_based(tx_data)
        if result["type"] == "UNKNOWN":
            result = classify_with_llm(tx_data)

    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()

What does this code do?

  • Imports classify_with_llm from your new module so the pipeline can call Claude when needed.
  • The --force-llm flag bypasses rule-based classification entirely. Useful for comparing the LLM's reasoning against your rules on known transactions.
  • Without the flag, the pipeline tries rules first. Only when rules return UNKNOWN does it call Claude. This keeps costs at zero for transactions your rules already handle.
  • Save classify.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before running the test below.

ⓧ I'd like to double check the full code

import sys
import os
import json
from fetcher import fetch_transaction
from classifier import classify_rule_based
from llm_classifier import classify_with_llm


def main():
    args = sys.argv[1:]
    if not args or args[0] in ("-h", "--help"):
        print("Usage: python classify.py <tx_hash> [--force-llm]")
        sys.exit(0)

    tx_hash = args[0]
    force_llm = "--force-llm" in args

    tx_data = fetch_transaction(tx_hash)

    if force_llm:
        result = classify_with_llm(tx_data)
    else:
        result = classify_rule_based(tx_data)
        if result["type"] == "UNKNOWN":
            result = classify_with_llm(tx_data)

    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()

Now run the same transaction hash that returned UNKNOWN earlier. Before you run it, predict: will Claude be able to classify it? What type do you think it is?

  • Run the classifier on the same transaction that returned UNKNOWN before:
python classify.py [[TX_HASH="the same transaction hash that returned UNKNOWN earlier"]]

This time you see a full classification with a type, protocol, summary, confidence level, reasoning, and "source": "LLM" in the output. Claude analyzed the transaction context and figured out what happened.

Seeing an authentication error?

Make sure your ANTHROPIC_API_KEY environment variable is still set in your current terminal session. If you opened a new terminal, you need to export it again.

help me debug an Anthropic API authentication error

  • Try the --force-llm flag on a transaction your rules would normally handle (like a simple ETH transfer) to compare Claude's classification against the deterministic one:
python classify.py [[TX_HASH_2="a transaction hash your rules classified successfully"]] --force-llm

Claude should agree with your rules on common transaction types, confirming the LLM and rule-based classifiers produce consistent results.

Your agent now handles any Ethereum transaction. Known patterns get instant, free classification. Unknown patterns get Claude's reasoning for fractions of a cent. Next up, you'll polish the output into a clean, scannable format with proper error handling.

Polish the Agent Output

Your classification pipeline works, but the output is raw JSON dumped to the terminal. That is fine for debugging, but not for a tool you can demo or pipe into other systems.

In this step you will add color-coded terminal output, input validation, error handling, and multiple output modes. The result is a CLI you can hand to someone and they immediately understand what each transaction did.

In this step, get ready to:
  • Add ANSI color formatting for scannable terminal output.
  • Add input validation and error handling for graceful failures.
  • Add summary, verbose, and JSON output modes.
Add color formatting helpers

ANSI escape codes let you print colored text in the terminal without any extra dependencies. You will define a color map and a helper function that wraps any string in the appropriate escape sequences.

  • Open classify.py and delete everything inside it.
  • Paste the following imports, color constants, and helper function:
import sys
import os
import json
from fetcher import fetch_transaction
from classifier import classify_rule_based
from llm_classifier import classify_with_llm

# ANSI escape codes for colored terminal output
COLORS = {
    "reset": "\033[0m",
    "bold": "\033[1m",
    "green": "\033[32m",
    "yellow": "\033[33m",
    "cyan": "\033[36m",
    "red": "\033[31m",
    "magenta": "\033[35m",
}


def colorize(text, color):
    """Wrap text in ANSI color codes for terminal display."""
    return f"{COLORS.get(color, '')}{text}{COLORS['reset']}"

What does this code do?

The COLORS dictionary maps friendly names like "green" to their ANSI escape sequences. Terminals interpret these sequences as color instructions.

The colorize() function wraps any text with the chosen color code and appends a reset code so subsequent text returns to normal.

  • Below colorize, add the print_result function that handles the default single-line summary output:
def print_result(result, tx_data, verbose=False):
    # Map each transaction type to a terminal color
    type_colors = {
        "SWAP": "cyan",
        "TRANSFER": "green",
        "APPROVAL": "yellow",
        "LENDING": "magenta",
        "NFT_TRADE": "magenta",
        "CONTRACT_DEPLOY": "yellow",
        "UNKNOWN": "red",
    }
    color = type_colors.get(result["type"], "reset")

    # Default mode: single-line summary
    if not verbose:
        protocol_str = f" | {result['protocol']}" if result.get("protocol") else ""
        print(
            f"{colorize(result['type'], color)}{protocol_str} | "
            f"{result.get('summary', 'No summary')} | "
            f"{result.get('confidence', '?')} confidence | "
            f"Source: {result['source']}"
        )
        return

What does this code do?

The print_result function takes the classification result and the raw transaction data. It picks a color based on the transaction type.

In non-verbose mode (the default), it prints a compact one-liner showing the type, protocol, summary, confidence, and classification source. The early return exits before the verbose section.

  • Save classify.py and verify the module imports cleanly by running this command:
python -c "from classify import colorize; print(colorize('COLORS WORKING', 'green'))"

You should see COLORS WORKING printed in green text.

Seeing a SyntaxError?

Make sure you deleted the old main() function and if __name__ block. Leftover code below your new functions can cause indentation or syntax errors.

Help me debug this SyntaxError

Build the verbose display mode

The verbose mode gives a full breakdown with labeled fields, token movements, and LLM cost estimates. This is the output you use when investigating a single transaction in detail.

  • Add the verbose output section directly below the return statement in print_result, at the same indentation level as the if not verbose: block:
    # Verbose mode: full detailed breakdown
    print(f"\n{'='*60}")
    print(f"  {colorize('TRANSACTION CLASSIFICATION', 'bold')}")
    print(f"{'='*60}")
    print(f"  Hash:       {tx_data['hash']}")
    print(f"  Type:       {colorize(result['type'], color)}")
    print(f"  Protocol:   {result.get('protocol') or 'Unknown'}")
    print(f"  Summary:    {result.get('summary') or 'N/A'}")
    print(f"  Confidence: {result.get('confidence') or '?'}")
    print(f"  Source:     {result['source']}")

    if result.get("reasoning"):
        print(f"  Reasoning:  {result['reasoning']}")

    # Show token movement details if available
    if tx_data.get("transfers"):
        print(f"\n  {colorize('Token Movements:', 'bold')}")
        for t in tx_data["transfers"][:5]:
            print(f"    {t['from'][:10]}... -> {t['to'][:10]}... ({t['token_address'][:10]}...)")

    # Show LLM cost estimate when applicable
    if result.get("tokens_used"):
        cost_estimate = result["tokens_used"] * 0.000001
        print(f"\n  LLM tokens: {result['tokens_used']} (~${cost_estimate:.6f})")

    print(f"{'='*60}\n")

What does this code do?

The verbose section prints a bordered box with each field on its own labeled line. It shows the hash, type (color-coded), protocol, summary, confidence, source, and optional reasoning from the LLM.

Token movements display up to 5 transfers with truncated addresses. The LLM cost estimate multiplies token count by a rough per-token cost so you can see what each classification costs.

Add error handling and output flags

A useful CLI needs to catch bad input before hitting the network. You will add format validation for the transaction hash, try/except around the RPC call, and flags to switch between output modes.

  • Add the main() function below print_result. This section parses arguments and validates the hash format:
def main():
    args = sys.argv[1:]

    # Show usage help
    if not args or args[0] in ("-h", "--help"):
        print("Usage: python classify.py <tx_hash> [--verbose] [--json] [--force-llm]")
        sys.exit(0)

    tx_hash = args[0]
    verbose = "--verbose" in args
    output_json = "--json" in args
    force_llm = "--force-llm" in args

    # Validate transaction hash format (0x + 64 hex characters)
    if not tx_hash.startswith("0x") or len(tx_hash) != 66:
        print(colorize("ERROR: Invalid transaction hash format. Must be 0x + 64 hex chars.", "red"))
        sys.exit(1)

What does this code do?

The argument parser extracts the transaction hash and checks for optional flags: --verbose for the full breakdown, --json for machine-readable output, and --force-llm to bypass rules and always use Claude.

The hash validation catches typos immediately. A valid Ethereum transaction hash is always 0x followed by exactly 64 hexadecimal characters.

  • Directly below the validation block (still inside main()), add the classification logic and output handling:
    # Fetch transaction, exit gracefully on failure
    try:
        tx_data = fetch_transaction(tx_hash)
    except Exception as e:
        print(colorize(f"ERROR: Failed to fetch transaction: {e}", "red"))
        sys.exit(1)

    # Run classification: rules first, LLM fallback if unknown
    if force_llm:
        result = classify_with_llm(tx_data)
    else:
        result = classify_rule_based(tx_data)
        if result["type"] == "UNKNOWN":
            result = classify_with_llm(tx_data)

    # Output in the requested format
    if output_json:
        output = {**result, "tx_hash": tx_hash}
        print(json.dumps(output, indent=2, default=str))
    else:
        print_result(result, tx_data, verbose=verbose)


if __name__ == "__main__":
    main()

What does this code do?

The try/except around fetch_transaction catches network timeouts, invalid hashes the RPC rejects, and any other failure. Instead of a traceback, the user sees a clean red error message.

The output section routes to JSON (for piping into other tools), verbose (for detailed investigation), or the default single-line summary. The if __name__ guard ensures the script runs when called directly.

  • Save classify.py.
  • Test the error handling first. Before you run this, what do you think happens when the hash format is wrong?
python classify.py invalidhash

You should see a red error message: ERROR: Invalid transaction hash format. Must be 0x + 64 hex chars.

Seeing a NameError or ImportError?

Check that colorize and print_result are defined ABOVE main() in the file. Python reads top to bottom, so functions must be defined before they are called.

Also confirm you have the if __name__ == "__main__": block at the very end of the file.

Help me fix my NameError or ImportError in classify.py

  • Now test with a real transaction hash. Go to etherscan.io and click on any recent transaction to copy its hash.
  • Run your agent with that hash to see the default single-line output:
python classify.py [[TX_HASH="0x + your 64 hex char transaction hash from etherscan"]]

You should see color-coded output on a single line showing the type, summary, confidence, and source.

  • Test the --verbose flag for the full detailed breakdown by running:
python classify.py [[TX_HASH="0x + your 64 hex char transaction hash from etherscan"]] --verbose

You should see a bordered box with labeled fields including Hash, Type, Protocol, Summary, Confidence, and Source. If the transaction involves token transfers, you will also see them listed under Token Movements.

  • Test the --json flag for machine-readable output by running:
python classify.py [[TX_HASH="0x + your 64 hex char transaction hash from etherscan"]] --json

This outputs the classification as a JSON object you can pipe into other tools or your future agent system.

Try different transaction types

On etherscan.io, try a Uniswap swap transaction, a simple ETH transfer, and a token approval to see different classification types and colors in action.

What about graceful degradation?

Your fetcher.py already handles the 4byte.directory API being down. The resolve_selector function wraps its HTTP call in a try/except and returns None on failure. The classifier still works with the raw selector data.

Similarly, if the Anthropic API fails, the try/except in main() catches the exception and prints a readable error instead of a traceback.

✔️ Awesome, I've got everything!

Great. Double check you have saved classify.py and confirmed both the error path and a successful classification work.

ⓧ I'd like to double check the full code

Your classify.py should contain these sections in order. Scroll through and confirm each is present:

  • Imports (sys, os, json, plus your three local modules).
  • COLORS dictionary with 7 entries (reset, bold, green, yellow, cyan, red, magenta).
  • colorize(text, color) helper function.
  • print_result(result, tx_data, verbose=False) with a non-verbose branch that returns early and a verbose branch with the bordered box.
  • main() with help text, flag parsing, hash validation, try/except fetch, classification pipeline, and JSON/verbose/default output routing.
  • if __name__ == "__main__": main() at the very end.

Your agent now has a polished interface with color-coded output, multiple modes, and error handling that fails gracefully. Next up, you will push it further with batch wallet analysis in the Secret Mission.

Secret mission

Batch Wallet Analysis

Your classifier handles one transaction at a time. What if you could point it at an entire wallet and get a breakdown of all recent activity? In this secret mission, you will add batch processing mode that accepts a wallet address, classifies its last N transactions, and produces a portfolio activity summary.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with no persistent cloud resources. Anthropic API calls are pay-per-use, costing approximately $0.01-0.10 total depending on how many transactions you classified with the LLM. There are no ongoing costs once you stop running the tool.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The tx-identifier project folder on your Desktop (contains all source code and the .venv virtual environment).
  • Anthropic API usage (pay-per-use, no ongoing charges).
  • Environment variables (ANTHROPIC_API_KEY, ETH_RPC_URL) set in your terminal session.

Keep everything running

No action needed. Choose this if you are still actively building or want to extend the agent with multi-chain support, real-time monitoring, or webhook alerts.

  • Your code stays in the tx-identifier folder on your Desktop.
  • No cloud resources are running. The Anthropic API only charges when you make calls.
  • Your virtual environment stays ready. Reactivate it any time with source .venv/bin/activate.

Pause - I'll come back to this later

Deactivate your virtual environment to free up your terminal session. Your project files and installed packages stay on disk, ready to resume.

  • Deactivate the virtual environment by running:
deactivate
  • Environment variables (ANTHROPIC_API_KEY, ETH_RPC_URL) only last for the current terminal session. They disappear when you close the terminal.
  • When you return, open a new terminal, cd into tx-identifier, reactivate the venv, and re-export your environment variables.

Delete - I don't want to use this again

Remove all project files, the virtual environment, and installed packages. This cannot be undone.

  • Delete the entire project folder by running:
cd ~/Desktop
rm -rf tx-identifier
  • This removes all source code (verify.py, fetcher.py, classifier.py, llm_classifier.py, classify.py) and the .venv virtual environment with all installed packages.
  • Unset the environment variables from your current session by running:
unset ANTHROPIC_API_KEY
unset ETH_RPC_URL
  • No cloud resources need to be deleted. PublicNode requires no account and has nothing to clean up. Anthropic API usage is pay-per-use with no subscription to cancel.

Nice Work!

Nice Work!

You just built a blockchain transaction identifier agent that can classify any Ethereum transaction by type, protocol, and intent.

You learned how to:

  • Decode raw on-chain data using function selectors, event logs, and the 4byte.directory signature database to extract meaning from transaction input data.
  • Implement the production pattern of a rule-based fast path for deterministic classification with an LLM fallback that handles ambiguous or novel transactions.
  • Build a modular agent architecture with swappable components (fetcher, classifier, LLM classifier) ready to extend into larger autonomous pipelines.
  • Secret Mission: Added batch wallet analysis to classify an address's entire recent activity and produce a portfolio activity summary.

Ready to quiz yourself?