Skip to main content

Make your own AI trading agent on Ethereum

AI
trading
agent
python
Intermediate
Ori Pomerantz
February 13, 2026
24 minute read

In this tutorial you learn how to build a simple AI trading agent. This agent works using these steps:

  1. Read the current and past prices of a token, as well as other potentially relevant information
  2. Build a query with this information, along with background information to explain how it might be relevant
  3. Submit the query and receive back a projected price
  4. Trade based on the recommendation
  5. Wait and repeat

This agent demonstrates how to read information, translate it into a query that yields a usable answer, and use that answer. All of these are steps required for an AI agent. This agent is implemented in Python because it is the most common language used in AI.

Why do this?

Automated trading agents allow developers to select and execute a trading strategy. AI agents allow for more complex and dynamic trading strategies, potentially using information and algorithms the developer has not even considered using.

The tools

This tutorial uses Python (opens in a new tab), the Web3 library (opens in a new tab), and Uniswap v3 (opens in a new tab) for quotes and trading.

Why Python?

The most widely used language for AI is Python (opens in a new tab), so we use it here. Don't worry if you don't know Python. The language is very clear, and I will explain exactly what it does.

The Web3 library (opens in a new tab) is the most common Python Ethereum API. It is pretty easy to use.

Trading on the blockchain

There are many distributed exchanges (DEX) that let you trade tokens on Ethereum. However, they tend to have similar exchange rates due to arbitrage.

Uniswap (opens in a new tab) is a widely used DEX that we can use for both quotes (to see token relative values) and trades.

OpenAI

For a large language model, I chose to get started with OpenAI (opens in a new tab). To run the application in this tutorial you'll need to pay for API access. The minimum payment of $5 is more than sufficient.

Development, step by step

To simplify development, we proceed in stages. Each step is a branch in GitHub.

Getting started

There are steps to get started under UNIX or Linux (including WSL (opens in a new tab))

  1. If you don't already have it, download and install Python (opens in a new tab).

  2. Clone the GitHub repository.

    git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    cd 260215-ai-agent
    
  3. Install uv (opens in a new tab). The command on your system might be different.

    pipx install uv
    
  4. Download the libraries.

    uv sync
    
  5. Activate the virtual environment.

    source .venv/bin/activate
    
  6. To verify Python and Web3 are working correctly, run python3 and provide it with this program. You can enter it at the >>> prompt; there is no need to create a file.

    from web3 import Web3
    MAINNET_URL = "https://eth.drpc.org"
    w3 = Web3(Web3.HTTPProvider(MAINNET_URL))
    w3.eth.block_number
    quit()
    

Reading from the blockchain

The next step is to read from the blockchain. To do that, you need to change to the 02-read-quote branch and then use uv to run the program.

git checkout 02-read-quote
uv run agent.py

You should receive a list of Quote objects, each with a timestamp, a price, and the asset (currently always WETH/USDC).

Here is a line-by-line explanation.

Import the libraries we need. They are explained below when used.

print = functools.partial(print, flush=True)

Replaces Python’s print with a version that always flushes output immediately. This is useful in a long-running script because we don't want to wait for status updates or debugging output.

MAINNET_URL = "https://eth.drpc.org"

A URL to get to mainnet. You can get one from Node as a service or use one of those advertised in Chainlist (opens in a new tab).

BLOCK_TIME_SECONDS = 12
MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
HOUR_BLOCKS = MINUTE_BLOCKS * 60
DAY_BLOCKS = HOUR_BLOCKS * 24

An Ethereum mainnet block typically happens every twelve seconds, so these are the number of blocks we'd expect to happen in a time period. Note that this is not an exact figure. When the block proposer is down, that block is skipped, and the time for the next block is 24 seconds. If we wanted to get the exact block for a timestamp, we'd use binary search (opens in a new tab). However, this is close enough for our purposes. Predicting the future is not an exact science.

CYCLE_BLOCKS = DAY_BLOCKS

The size of the cycle. We review quotes once per cycle and try to estimate the value at the end of the next cycle.

# The address of the pool we're reading
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

The quote values are taken from the Uniswap 3 USDC/WETH pool at address 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). This address is already in checksum form, but it's better to use Web3.to_checksum_address (opens in a new tab) to make the code reusable.

These are the ABIs (opens in a new tab) for the two contracts we need to contact. To keep the code concise, we include only the functions we need to call.

w3 = Web3(Web3.HTTPProvider(MAINNET_URL))

Initiate the Web3 (opens in a new tab) library and connect to an Ethereum node.

@dataclass(frozen=True)
class ERC20Token:
    address: str
    symbol: str
    decimals: int
    contract: Contract

This is one way to create a data class in Python. The Contract (opens in a new tab) data type is used to connect to the contract. Note the (frozen=True). In Python booleans (opens in a new tab) are defined as True or False, capitalized. This data class is frozen, meaning the fields cannot be modified.

Note the indentation. In contrast to C-derived languages (opens in a new tab), Python uses indentation to denote blocks. The Python interpreter knows that the following definition is not part of this data class because it doesn't start at the same indentation as the data class fields.

@dataclass(frozen=True)
class PoolInfo:
    address: str
    token0: ERC20Token
    token1: ERC20Token
    contract: Contract
    asset: str
    decimal_factor: Decimal = 1

The Decimal (opens in a new tab) type is used for accurately handling decimal fractions.

    def get_price(self, block: int) -> Decimal:

This is the way to define a function in Python. The definition is indented to show it is still part of PoolInfo.

In a function that is part of a data class the first parameter is always self, the data class instance that called here. Here there is another parameter, the block number.

        assert block <= w3.eth.block_number, "Block is in the future"

If we could read the future, we wouldn't need AI for trading.

        sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])

The syntax for calling a function on the EVM from Web3 is this: <contract object>.functions.<function name>().call(<parameters>). The parameters can be the EVM function's parameters (if any; here there aren't) or named parameters (opens in a new tab) for modifying blockchain behavior. Here we use one, block_identifier, to specify the block number we wish to run in.

The result is this struct, in array form (opens in a new tab). The first value is a function of the exchange rate between the two tokens.

        raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2

To reduce onchain calculations, Uniswap v3 does not store the actual exchange factor but rather its square root. Because the EVM does not support floating point math or fractions, instead of the actual value, the response is price296

         # (token1 per token0)
        return 1/(raw_price * self.decimal_factor)

The raw price we get is the number of token0 we get for each token1. In our pool token0 is USDC (stablecoin with the same value as a US dollar) and token1 is WETH (opens in a new tab). The value we really want is the number of dollars per WETH, not the inverse.

The decimal factor is the ratio between the decimal factors (opens in a new tab) for the two tokens.

@dataclass(frozen=True)
class Quote:
    timestamp: str
    price: Decimal
    asset: str

This data class represents a quote: the price of a specific asset at a given point in time. At this point, the asset field is irrelevant because we use a single pool and therefore have a single asset. However, we will add more assets later.

This function takes an address and returns information about the token contract at that address. To create a new Web3 Contract (opens in a new tab), we provide the address and ABI to w3.eth.contract.

This function returns everything we need about a specific pool (opens in a new tab). The syntax f"<string>" is a formatted string (opens in a new tab).

def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:

Get a Quote object. The default value for block_number is None (no value).

    if block_number is None:
        block_number = w3.eth.block_number

If a block number was not specified, use w3.eth.block_number, which is the latest block number. This is the syntax for an if statement (opens in a new tab).

It might look as if it would have been better to just set the default to w3.eth.block_number, but that doesn't work well because it would be the block number at the time the function is defined. In a long-running agent, this would be a problem.

    block = w3.eth.get_block(block_number)
    price = pool.get_price(block_number)
    return Quote(
        timestamp=datetime.fromtimestamp(block.timestamp, timezone.utc).isoformat(),
        price=price.quantize(Decimal("0.01")),
        asset=pool.asset
    )

Use the datetime library (opens in a new tab) to format it to a format readable for humans and large language models (LLMs). Use Decimal.quantize (opens in a new tab) to round the value to two decimal places.

def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:

In Python you define a list (opens in a new tab) that can only contain a specific type using list[<type>].

    quotes = []
    for block in range(start_block, end_block + 1, step):

In Python a for loop (opens in a new tab) typically iterates over a list. The list of block numbers to find quotes in comes from range (opens in a new tab).

        quote = get_quote(pool, block)
        quotes.append(quote)
    return quotes

For each block number, get a Quote object and append it to the quotes list. Then return that list.

This is the main code of the script. Read the pool information, get twelve quotes, and pprint (opens in a new tab) them.

Creating a prompt

Next, we need to convert this list of quotes into a prompt for an LLM and obtain an expected future value.

git checkout 03-create-prompt
uv run agent.py

The output is now going to be a prompt to an LLM, similar to:

Notice that there are quotes for two assets here, WETH/USDC and WBTC/WETH. Adding quotes from another asset might improve the prediction accuracy.

What a prompt looks like

This prompt contains three sections, which are pretty common in LLM prompts.

  1. Information. LLMs have a lot of information from their training, but they usually don't have the latest. This is the reason we need to retrieve the latest quotes here. Adding information to a prompt is called retrieval augmented generation (RAG) (opens in a new tab).

  2. The actual question. This is what we want to know.

  3. Output formatting instructions. Normally, an LLM will give us an estimate with an explanation of how it arrived at it. This is better for humans, but a computer program just needs the bottom line.

Code explanation

Here is the new code.

from datetime import datetime, timezone, timedelta

We need to provide the LLM with the time for which we want an estimate. To get a time "n minutes/hours/days" in the future, we use the timedelta class (opens in a new tab).

# The addresses of the pools we're reading
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

We have two pools we need to read.

In the WETH/USDC pool, we want to know how many of token0 (USDC) we need to buy one of token1 (WETH). In the WETH/WBTC pool, we want to know how many token1 (WETH) we need to buy one token0 (WBTC, which is wrapped Bitcoin). We need to track whether the pool’s ratio needs to be reversed.

To know if a pool needs to be reversed, we get to get that as input to read_pool. Also, the asset symbol needs to be set up correctly.

The syntax <a> if <b> else <c> is the Python equivalent of the ternary conditional operator (opens in a new tab), which in a C-derived language would be <b> ? <a> : <c>.

def format_quotes(quotes: list[Quote]) -> str:
    result = f"Asset: {quotes[0].asset}\n"
    for quote in quotes:
        result += f"\t{quote.timestamp[0:16]} {quote.price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)}\n"
    return result

This function builds a string that formats a list of Quote objects, assuming they all apply to the same asset.

def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:
    return f"""

In Python multi-line string literals (opens in a new tab) are written as """ .... """.

Given these quotes:
{
    functools.reduce(lambda acc, q: acc + '\n' + q,
        map(lambda q: format_quotes(q), quotes))
}

Here, we use the MapReduce (opens in a new tab) pattern to generate a string for each quote list with format_quotes, then reduce them into a single string for use in the prompt.

What would you expect the value for {asset} to be at time {expected_time}?

Provide your answer as a single number rounded to two decimal places,
without any other text.
    """

The rest of the prompt is as expected.

Review the two pools and obtain quotes from both.

future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]

print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))

Determine the future time point for which we want the estimate, and create the prompt.

Interfacing with an LLM

Next, we prompt an actual LLM and receive an expected future value. I wrote this program using OpenAI, so if you want to use a different provider, you'll need to adjust it.

  1. Get an OpenAI account (opens in a new tab)

  2. Fund the account (opens in a new tab)—the minimum amount at the time of writing is $5

  3. Create an API key (opens in a new tab)

  4. In the command line, export the API key so your program can use it

    export OPENAI_API_KEY=sk-<the rest of the key goes here>
    
  5. Checkout and run the agent

    git checkout 04-interface-llm
    uv run agent.py
    

Here is the new code.

from openai import OpenAI

open_ai = OpenAI()  # The client reads the OPENAI_API_KEY environment variable

Import and instantiate the OpenAI API.

response = open_ai.chat.completions.create(
    model="gpt-4-turbo",
    messages=[
        {"role": "user", "content": prompt}
    ],
    temperature=0.0,
    max_tokens=16,
)

Call the OpenAI API (open_ai.chat.completions.create) to create the response.

Output the price and provide a buy or sell recommendation.

Testing the predictions

Now that we can generate predictions, we can also use historical data to assess whether we produce useful predictions.

uv run test-predictor.py

The expected result is similar to:

Most of the tester is identical to the agent, but here are the parts that are new or modified.

We look at CYCLES_FOR_TEST (specified as 40 here) days back.

# Create predictions and check them against real history

total_error = Decimal(0)
changes = []

There are two types of errors we are interested in. The first, total_error, is simply the sum of errors the predictor made.

To understand the second, changes, we need to remember the agent's purpose. It's not to predict the WETH/USDC ratio (ETH price). It's to issue sell and buy recommendations. If the price is currently $2000 and it predicts $2010 tomorrow, we don't mind if the actual result is $2020 and we earn extra money. But we do mind if it predicted $2010, and bought ETH based on that recommendation, and the price drops to $1990.

for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):

We can only look at cases where the complete history (the values used for the prediction and the real-world value to compare it to) is available. This means the newest case must be the one that started CYCLES_BACK ago.

    wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]
    wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]

Use slices (opens in a new tab) to get the same number of samples as the number the agent uses. The code between here and the next segment is the same get-a-prediction code we have in the agent.

    predicted_price = Decimal(response.choices[0].message.content.strip())
    real_price = wethusdc_quotes[index+CYCLES_BACK].price
    prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].price

Get the predicted price, the real price, and the price at the time of the prediction. We need the price at the time of the prediction to determine whether the recommendation was to buy or sell.

    error = abs(predicted_price - real_price)
    total_error += error
    print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {error} USD")

Figure the error, and add it to the total.

    recomended_action = 'buy' if predicted_price > prediction_time_price else 'sell'
    price_increase = real_price - prediction_time_price
    changes.append(price_increase if recomended_action == 'buy' else -price_increase)

For changes, we want the monetary impact of buying or selling one ETH. So first, we need to determine the recommendation, then assess how the actual price changed, and whether the recommendation made money (positive change) or cost money (negative change).

print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")

length_changes = Decimal(len(changes))
mean_change = sum(changes, Decimal(0)) / length_changes
print (f"Mean change per recommendation: {mean_change} USD")
var = sum((x - mean_change) ** 2 for x in changes) / length_changes
print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")

Report the results.

print (f"Profitable days: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
print (f"Losing days: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")

Use filter (opens in a new tab) to count the number of profitable days and the number of costly days. The result is a filter object, which we need to convert to a list to get the length.

Submitting transactions

Now we need to actually submit transactions. However, I don't want to spend real money at this point, before the system is proven. Instead, we will create a local fork of mainnet, and "trade" on that network.

Here are the steps to create a local fork and enable trading.

  1. Install Foundry (opens in a new tab)

  2. Start anvil (opens in a new tab)

    anvil --fork-url https://eth.drpc.org --block-time 12
    

    anvil is listening on the default URL for Foundry, http://localhost:8545 (opens in a new tab), so we don't need to specify the URL for the cast command (opens in a new tab) we use to manipulate the blockchain.

  3. When running in anvil, there are ten test accounts that have ETH—set the environment variables for the first one

    PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    ADDRESS=`cast wallet address $PRIVATE_KEY`
    
  4. These are the contracts we need to use. SwapRouter (opens in a new tab) is the Uniswap v3 contract we use to actually trade. We could trade directly through the pool, but this is much easier.

    The two bottom variables are the Uniswap v3 paths required to swap between WETH and USDC.

    WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    
  5. Each of the test accounts has 10,000 ETH. Use the WETH contract to wrap 1000 ETH to obtain 1000 WETH for trading.

    cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
    
  6. Use SwapRouter to trade 500 WETH for USDC.

    cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY
    MAXINT=`cast max-int uint256`
    cast send $SWAP_ROUTER \
        "exactInput((bytes,address,uint256,uint256,uint256))" \
        "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \
        --private-key $PRIVATE_KEY
    

    The approve call creates an allowance that allows SwapRouter to spend some of our tokens. Contracts cannot monitor events, so if we transfer tokens directly to the SwapRouter contract, it wouldn't know it was paid. Instead, we permit the SwapRouter contract to spend a certain amount, and then SwapRouter does it. This is done through a function called by SwapRouter, so it knows if it was successful.

  7. Verify you have enough of both tokens.

    cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei
    echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc
    

Now that we have WETH and USDC, we can actually run the agent.

git checkout 05-trade
uv run agent.py

The output will look similar to:

To actually use it, you need a few minor changes.

  • In line 14, change MAINNET_URL to a real access point, such as https://eth.drpc.org
  • In line 28, change PRIVATE_KEY to your own private key
  • Unless you are very wealthy and can buy or sell 1 ETH each day for an unproven agent, you might want to change 29 to decrease WETH_TRADE_AMOUNT

Code explanation

Here is the new code.

SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")
WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

The same variables we used in step 4.

WETH_TRADE_AMOUNT=1

The amount to trade.

ERC20_ABI = [
    { "name": "symbol", ... },
    { "name": "decimals", ... },
    { "name": "balanceOf", ...},
    { "name": "approve", ...}
]

To actually trade, we need the approve function. We also want to show balances before and after, so we also need balanceOf.

SWAP_ROUTER_ABI = [
  { "name": "exactInput", ...},
]

In the SwapRouter ABI we just need exactInput. There is a related function, exactOutput, we could use to buy exactly one WETH, but for simplicity we just use exactInput in both cases.

account = w3.eth.account.from_key(PRIVATE_KEY)
swap_router = w3.eth.contract(
    address=SWAP_ROUTER_ADDRESS,
    abi=SWAP_ROUTER_ABI
)

The Web3 definitions for the account (opens in a new tab) and the SwapRouter contract.

def txn_params() -> dict:
    return {
        "from": account.address,
        "value": 0,
        "gas": 300000,
        "nonce": w3.eth.get_transaction_count(account.address),
    }

The transaction parameters. We need a function here because the nonce (opens in a new tab) must change each time.

def approve_token(contract: Contract, amount: int):

Approve a token allowance for SwapRouter.

    txn = contract.functions.approve(SWAP_ROUTER_ADDRESS, amount).build_transaction(txn_params())
    signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)
    tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)

This is how we send a transaction in Web3. First we use the Contract object (opens in a new tab) to build the transaction. Then we use web3.eth.account.sign_transaction (opens in a new tab) to sign the transaction, using PRIVATE_KEY. Finally, we use w3.eth.send_raw_transaction (opens in a new tab) to send the transaction.

    print(f"Approve transaction sent: {tx_hash.hex()}")
    w3.eth.wait_for_transaction_receipt(tx_hash)
    print("Approve transaction mined.")

w3.eth.wait_for_transaction_receipt (opens in a new tab) waits until the transaction is mined. It returns the receipt if needed.

SELL_PARAMS = {
    "path": WETH_TO_USDC,
    "recipient": account.address,
    "deadline": 2**256 - 1,
    "amountIn": WETH_TRADE_AMOUNT * 10 ** wethusdc_pool.token1.decimals,
    "amountOutMinimum": 0,
}

These are the parameters when selling WETH.

def make_buy_params(quote: Quote) -> dict:
    return {
        "path": USDC_TO_WETH,
        "recipient": account.address,
        "deadline": 2**256 - 1,
        "amountIn": int(quote.price*WETH_TRADE_AMOUNT) * 10**wethusdc_pool.token0.decimals,
        "amountOutMinimum": 0,
    }

In contrast to SELL_PARAMS, the buy parameters can change. The input amount is the cost of 1 WETH, as available in quote.

The buy() and sell() functions are nearly identical. First we approve a sufficient allowance for SwapRouter, and then we call it with the correct path and amount.

def balances():
    token0_balance = wethusdc_pool.token0.contract.functions.balanceOf(account.address).call()
    token1_balance = wethusdc_pool.token1.contract.functions.balanceOf(account.address).call()

    print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")
    print(f"{wethusdc_pool.token1.symbol} Balance: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")

Report user balances in both currencies.

This agent currently only works once. However, you can change it to work continuously either by running it from crontab (opens in a new tab) or by wrapping lines 368-400 in a loop and using time.sleep (opens in a new tab) to wait until it is time for the next cycle.

Possible improvements

This is not a full production version; it is merely an example to teach the basics. Here are some ideas for improvements.

Smarter trading

There are two important facts the agent ignores when deciding what to do.

  • The magnitude of anticipated change. The agent sells a fixed amount of WETH if the price is expected to decline, regardless of the magnitude of the decline. Arguably, it would be better to ignore minor changes and sell based on how much we expect the price to decline.
  • The current portfolio. If 10% of your portfolio is in WETH and you think the price will go up, it probably makes sense to buy more. But if 90% of your portfolio is in WETH, you may be sufficiently exposed, and there is no need to buy more. The reverse is true if you expect the price to go down.

What if you want to keep your trading strategy a secret?

AI vendors can see the queries you send to their LLMs, which could expose the genius trading system you developed with your agent. A trading system that too many people use is worthless because too many people try to buy when you want to buy (and the price goes up) and try to sell when you want to sell (and the price goes down).

You can run an LLM locally, for example, using LM-Studio (opens in a new tab), to avoid this problem.

From AI bot to AI agent

You can make a good case that this is an AI bot, not an AI agent. It implements a relatively simple strategy that relies on predefined information. We can enable self-improvement, for example, by providing a list of Uniswap v3 pools and their latest values and asking which combination has the best predictive value.

Slippage protection

Currently there is no slippage protection (opens in a new tab). If the current quote is $2000, and the expected price is $2100, the agent will buy. However, if before the agent buys the cost rises to $2200, it makes no sense to buy anymore.

To implement slippage protection, specify an amountOutMinimum value in lines 325 and 334 of agent.py (opens in a new tab).

Conclusion

Hopefully, now you know enough to get started with AI agents. This is not a comprehensive overview of the subject; there are whole books dedicated to that, but this is enough to get you started. Good luck!

See here for more of my work (opens in a new tab).

Page last update: March 3, 2026

Was this tutorial helpful?