- Node.js and npm installed (for Solidity tooling like Hardhat)
- Python 3.9+ with
web3.py and relevant trading SDKs (like Freqtrade or Hummingbot)
- Access to an Ethereum testnet (Goerli recommended) via RPC provider
- Solidity v0.8.x compiler
Install core dependencies for the Solidity environment:
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers
And for Python:
pip install web3 eth-account
I used Hardhat 2.x in this build, which has a stable plugin ecosystem and a JS-friendly experience. This tutorial assumes some familiarity with Solidity and Python basics but stays focused on the integration.
Designing the Trading Bot Architecture
The bot architecture has two main components:
- Solidity smart contract: acts as the execution layer. This contract manages swaps and enforces constraints like maximum slippage.
- Python autonomous agent: handles strategy logic, monitoring on-chain data, running signals, and triggering contract calls.
This separation means your trading logic runs off-chain (in Python), but sensitive trade executions happen on-chain, reducing attack surfaces.
Key design decisions:
- The bot’s wallet is a contract wallet (account abstraction could be applied, e.g., ERC-4337)
- Trade execution uses Uniswap V3’s router contract through safe interface calls
- Spending approval for tokens is minimized with session keys and spending limits
- Integration with an off-chain price feed or oracle (for better swap decisions)
Building the Smart Contract in Solidity
Let’s start with a simple Solidity contract to swap tokens via Uniswap V3.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
contract TradingBot {
ISwapRouter public immutable swapRouter;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor(address _swapRouter) {
swapRouter = ISwapRouter(_swapRouter);
owner = msg.sender;
}
function swapExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountIn,
uint256 amountOutMinimum
) external onlyOwner returns (uint256 amountOut) {
// Transfer the amountIn tokens from the owner to this contract
require(
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn),
"Transfer failed"
);
// Approve the router to spend tokenIn
IERC20(tokenIn).approve(address(swapRouter), amountIn);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: owner,
deadline: block.timestamp + 300,
amountIn: amountIn,
amountOutMinimum: amountOutMinimum,
sqrtPriceLimitX96: 0
});
amountOut = swapRouter.exactInputSingle(params);
}
}
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
}
This contract lets the owner swap a precise input amount of one token to another via Uniswap V3. It restricts execution to owner only, preventing unauthorized calls.
Why this approach? By limiting logic on-chain, you reduce bot complexity and gas costs. The Python agent decides when to trigger the swap, the contract just executes.
Developing the Autonomous Trading Agent in Python
Now, onto the Python side. This agent monitors price signals (for example, from TheGraph or on-chain oracles) and triggers swaps when conditions meet your strategy.
Below is a minimalist example to send a swap tx:
from web3 import Web3
from eth_account import Account
import json
import os
## Load config
rpc_url = os.getenv("RPC_URL") # Set your Goerli RPC URL
private_key = os.getenv("BOT_PRIVATE_KEY") # Bot wallet private key
w3 = Web3(Web3.HTTPProvider(rpc_url))
account = Account.from_key(private_key)
## Load compiled contract JSON
with open('./artifacts/contracts/TradingBot.sol/TradingBot.json') as f:
contract_json = json.load(f)
contract_address = '0xYourDeployedContractAddress'
contract = w3.eth.contract(address=contract_address, abi=contract_json['abi'])
## Prepare transaction
token_in = Web3.to_checksum_address("0xTokenInAddress")
token_out = Web3.to_checksum_address("0xTokenOutAddress")
fee = 3000 # 0.3% pool fee
amount_in = w3.to_wei(0.01, 'ether')
amount_out_minimum = 0 # For tutorial, always better set slippage
nonce = w3.eth.get_transaction_count(account.address)
tx = contract.functions.swapExactInputSingle(
token_in,
token_out,
fee,
amount_in,
amount_out_minimum
).buildTransaction({
'from': account.address,
'nonce': nonce,
'gas': 200000,
'gasPrice': w3.to_wei('30', 'gwei')
})
## Sign and send
signed_tx = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"Swap transaction sent: {tx_hash.hex()}")
## Optionally wait for receipt
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Transaction confirmed in block {receipt.blockNumber}")
In my experience, remember to set appropriate gas limits and monitor slippage carefully. Hardcoding amount_out_minimum to zero is a huge security risk — in production, calculate this based on price oracles or your own logic.
And one small trick: modularize triggers and execution in separate processes or threads to avoid bottlenecks.
Integrating On-Chain Swaps via Uniswap
While the contract uses exactInputSingle to execute swaps through Uniswap V3, customizing it to support multi-hop swaps or other pools requires familiarity with Uniswap’s router API.
The parameters:
tokenIn and tokenOut: ERC-20 addresses
fee: the specific pool fee tier (e.g., 500, 3000, 10000)
amountIn and amountOutMinimum: swap inputs
sqrtPriceLimitX96: optional price limit buffer (usually zero)
This contract setup makes a straightforward swap, but advanced bot strategies might bundle multiple swaps or integrate flash swaps. That kind of complexity calls for more on-chain state management, which tends to increase development risk.
I usually recommend starting simple and iterating.
Security Considerations and Best Practices
When building autonomous DeFi trading agents, security is always front and center.
- Never expose your wallet private keys in code or repositories; use environment variables and secure vaults
- Limit token approvals: if your bot uses session keys or limited-spending wallets, do not grant unlimited allowance
- Avoid reentrancy by minimizing external calls in Solidity or use established contracts following OpenZeppelin standards
- Validate all input parameters on-chain to mitigate front-running attacks
- Test thoroughly on testnets before mainnet deployment
More on wallet management and agent security is covered in our agent wallet security deep dive.
MEV Trading Bot Development Insights
MEV (Miner Extractable Value) techniques, such as frontrunning and sandwich trading, can enhance bot profitability but require advanced tooling and a strong security mindset.
Integrating MEV strategies with your Python agent often means:
- Bundling transactions using Flashbots or similar relay systems
- Monitoring mempool for profitable opportunities
- Reacting within blocks, requiring low-latency RPC and efficient RPC batching
Both the smart contract and Python agent need to be designed for quick execution and fail-safe rollback mechanisms.
But keep in mind — MEV interactions significantly increase complexity and risk, especially on mainnet.
Framework Comparison: Freqtrade vs Hummingbot
Choosing a foundation for your trading agent depends on your stack preference and feature needs.
| Feature |
Freqtrade |
Hummingbot |
| Language |
Python |
Python |
| Focus |
Crypto spot & derivatives bot |
AMM arbitrage, market making |
| On-chain integration |
Limited (mostly centralized APIs) |
Supports Uniswap & other AMMs |
| Community & Documentation |
Large & active |
Growing |
| License |
MIT |
Apache-2.0 |
| Maturity |
Stable; production-ready |
Mature, with DeFi focus |
If you want greater control over on-chain swaps and strategy layering, Hummingbot might be the better starting point. But if you prioritize quick iterations and broader asset types, Freqtrade offers a good playground.
Keep in mind many builders adapt these frameworks heavily or build minimal custom agents when using smart contracts for execution.
Troubleshooting Common Issues
Some developer pain points I’ve encountered:
- Transaction runs out of gas: Increase gas limit; estimate gas with
eth_estimateGas
- Swap reverts with deadline passed: Sync system clock and use block timestamps carefully
transferFrom failures: Confirm token approval and wallet balances
- Nonce errors in Python scripts: Use pending nonce or queue mechanisms to avoid race conditions
And a quick tip: logging full RPC request/response data helps identify subtle provider errors.
Conclusion and Next Steps
Building an autonomous DeFi trading bot combining Python and Solidity is fully achievable with solid fundamentals in both worlds. The key is clear separation of concerns: keep complex logic off-chain, rely on secure, minimal on-chain contracts for execution, and integrate trusted price oracles.
Start with the minimal swap contract and Python agent example here, then gradually add your strategy, security controls, and monitoring. I personally recommend exploring account abstraction (ERC-4337) in future iterations to simplify wallet management.
For further reading, our onchain AI agent setup and framework comparison pages provide complementary insights.
Happy bot building!