DEV Community

Naren karthi
Naren karthi

Posted on

How to Build an Autonomous Trading Agent with Python

🎓 End‑to‑End Tutorial

Build a Crypto‑AI dApp – “Pay‑per‑Use AI Text Generator”

In this tutorial you’ll create a decentralised application (dApp) that lets anyone pay a small amount of an ERC‑20 token (e.g. USDC on Sepolia) to receive a response from an AI language model (a GPT‑2‑style text generator).

The project combines three core blocks:

Layer Tech What it does
Blockchain Solidity, Hardhat, Sepolia testnet, MetaMask Smart contract that collects payment, records a request, and emits an event
AI Backend Python, Flask, 🤗 Transformers (GPT‑2), Web3.py Listens for contract events, verifies payment, runs the model, returns the text
Frontend HTML + Vanilla JS, Ethers.js UI for users to connect wallet, send a request, and view the AI answer

By the end you will have a fully functional dApp that you can run locally and then deploy to the cloud (Vercel/Render).


📚 Prerequisites

Skill Minimum level
JavaScript/TypeScript Comfortable with Node.js + npm
Solidity Know basic contract structure, events, modifiers
Python Able to install packages, write a Flask server
Git Basic clone / commit workflow
Docker (optional) For running a local Ethereum node (Ganache)
MetaMask Browser wallet installed and funded with Sepolia ETH/USDC

System Packages

OS Commands
Ubuntu / Debian sudo apt-get update && sudo apt-get install -y build-essential git curl
macOS Install Xcode command‑line tools (xcode-select --install) and Homebrew (/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)")
Windows Use WSL2 (Ubuntu) or install Git Bash + Node.js installer

Software Stack

Tool Version (as of Aug 2026) Install command
Node.js ≥ 20.x nvm install 20 && nvm use 20
npm ≥ 10.x (bundled) npm i -g npm@latest
Hardhat 2.22.x npm install --save-dev hardhat
Python ≥ 3.11 pyenv install 3.11 && pyenv global 3.11
pip ≥ 24.x python -m ensurepip --upgrade
Poetry (optional) 1.8.x `curl -sSL https://install.python-poetry.org
Flask 3.0.x {% raw %}pip install flask
transformers 4.44.x pip install transformers torch
Web3.py 6.20.x pip install web3
Ethers.js 6.11.x npm install ethers
MetaMask latest Chrome/Firefox extension
Docker (optional) 27.x Install from Docker Desktop

📂 Project Layout

crypto‑ai-dapp/
├─ contracts/
│   └─ PayPerUseAI.sol
├─ scripts/
│   └─ deploy.js
├─ test/
│   └─ PayPerUseAI.test.js
├─ backend/
│   ├─ app.py
│   └─ requirements.txt
├─ frontend/
│   ├─ index.html
│   └─ app.js
├─ hardhat.config.js
└─ README.md
Enter fullscreen mode Exit fullscreen mode

1️⃣ Smart Contract – PayPerUseAI.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title Pay‑per‑Use AI Service
/// @notice Users pay an ERC‑20 token to request AI text generation.
contract PayPerUseAI {
    // ------------------------------------------------------------------------
    //  State
    // ------------------------------------------------------------------------
    address public owner;               // contract owner (receives fees)
    IERC20 public paymentToken;         // e.g. USDC on Sepolia
    uint256 public price;               // price per request (token decimals)

    // Mapping to avoid replay attacks (requestId => used)
    mapping(bytes32 => bool) public usedRequests;

    // ------------------------------------------------------------------------
    //  Events
    // ------------------------------------------------------------------------
    event RequestGenerated(
        address indexed requester,
        bytes32 indexed requestId,
        string prompt,
        uint256 timestamp
    );

    // ------------------------------------------------------------------------
    //  Modifiers
    // ------------------------------------------------------------------------
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    // ------------------------------------------------------------------------
    //  Constructor
    // ------------------------------------------------------------------------
    constructor(address _paymentToken, uint256 _price) {
        owner = msg.sender;
        paymentToken = IERC20(_paymentToken);
        price = _price;
    }

    // ------------------------------------------------------------------------
    //  Public API
    // ------------------------------------------------------------------------
    /// @notice Pay and request AI generation.
    /// @param prompt The user supplied text prompt (max 256 bytes).
    function requestAI(string calldata prompt) external {
        require(bytes(prompt).length > 0 && bytes(prompt).length <= 256,
                "Prompt length invalid");

        // Transfer tokens from the caller to the contract.
        require(
            paymentToken.transferFrom(msg.sender, address(this), price),
            "Token transfer failed"
        );

        // Create a unique request id (hash of caller + prompt + block.timestamp)
        bytes32 requestId = keccak256(
            abi.encodePacked(msg.sender, prompt, block.timestamp)
        );

        // Mark as used (prevents double‑processing)
        usedRequests[requestId] = true;

        emit RequestGenerated(msg.sender, requestId, prompt, block.timestamp);
    }

    // ------------------------------------------------------------------------
    //  Owner Functions
    // ------------------------------------------------------------------------
    /// @notice Withdraw accumulated tokens.
    function withdraw() external onlyOwner {
        uint256 bal = paymentToken.balanceOf(address(this));
        require(bal > 0, "Zero balance");
        require(paymentToken.transfer(owner, bal), "Withdraw failed");
    }

    /// @notice Update price.
    function setPrice(uint256 _price) external onlyOwner {
        price = _price;
    }

    /// @notice Update payment token (e.g. switch to a newer USDC address).
    function setPaymentToken(address _token) external onlyOwner {
        paymentToken = IERC20(_token);
    }
}

// Minimal ERC‑20 interface
interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}
Enter fullscreen mode Exit fullscreen mode

Explanation

Part What it does
owner / paymentToken / price Stores contract admin, which ERC‑20 token is used, and the per‑request fee (in token base units).
usedRequests Guarantees that each request ID can be processed only once – the backend will check this flag before sending a response.
requestAI() - Verifies prompt length.
- Pulls price tokens from the caller (requires prior approve).
- Emits RequestGenerated containing the prompt and a unique requestId.
withdraw() Owner can pull the collected tokens.
setPrice / setPaymentToken Admin can change fee or token address without redeploying.

2️⃣ Hardhat Setup

2.1 Initialise the project

mkdir crypto-ai-dapp && cd crypto-ai-dapp
npm init -y
npm i --save-dev hardhat ethers @nomicfoundation/hardhat-toolbox dotenv
npx hardhat          # choose "Create a basic sample project"
Enter fullscreen mode Exit fullscreen mode

Delete the sample contract and copy PayPerUseAI.sol into contracts/.

2.2 Environment variables (.env)

# .env (never commit this file!)
SEPOLIA_RPC_URL="https://sepolia.infura.io/v3/<INFURA_PROJECT_ID>"
PRIVATE_KEY="0xYOUR_WALLET_PRIVATE_KEY"
USDC_SEPOLIA="0x6f9c4e196cdd8c0e2ab8ab9be0cbd6e5c4c36724"   # Sepolia USDC (example)
Enter fullscreen mode Exit fullscreen mode

2.3 Hardhat config (hardhat.config.js)

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.24",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
      // optional: gasPrice: 10e9
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

2.4 Deploy script (scripts/deploy.js)

const { ethers } = require("hardhat");
require("dotenv").config();

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying from:", deployer.address);

  const PayPerUseAI = await ethers.getContractFactory("PayPerUseAI");
  const price = ethers.parseUnits("0.5", 6); // 0.5 USDC (6 decimals)

  const contract = await PayPerUseAI.deploy(process.env.USDC_SEPOLIA, price);
  await contract.waitForDeployment();

  console.log("PayPerUseAI deployed to:", contract.target);
}

main()
  .then(() => process.exit(0))
  .catch((e) => {
    console.error(e);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

2.5 Deploy

npx hardhat run scripts/deploy.js --network sepolia
Enter fullscreen mode Exit fullscreen mode

Copy the resulting contract address – you’ll need it for the backend & frontend.


3️⃣ Backend – Flask AI Service

The backend listens for RequestGenerated events, verifies that the request hasn’t been processed, runs a GPT‑2 model, and returns the generated text via a simple HTTP endpoint.

3.1 Set up a virtual environment

cd backend
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Create requirements.txt:

flask
torch
transformers
web3
python-dotenv
Enter fullscreen mode Exit fullscreen mode

3.2 Environment (backend/.env)

# Contract & blockchain
CONTRACT_ADDRESS=0xYourDeployedAddress
SEPOLIA_RPC=https://sepolia.infura.io/v3/<INFURA_PROJECT_ID>
USDC_ADDRESS=0x6f9c4e196cdd8c0e2ab8ab9be0cbd6e5c4c36724

# Model (you can replace with a larger model later)
MODEL_NAME=gpt2
MAX_LENGTH=64
Enter fullscreen mode Exit fullscreen mode

3.3 Flask app (backend/app.py)

import os
import json
import hashlib
from datetime import datetime
from flask import Flask, request, jsonify
from dotenv import load_dotenv
from web3 import Web3
from transformers import pipeline, set_seed

load_dotenv()
app = Flask(__name__)

# -------------------------------------------------
# 1️⃣ Web3 / Contract Setup
# -------------------------------------------------
w3 = Web3(Web3.HTTPProvider(os.getenv("SEPOLIA_RPC")))
assert w3.is_connected(), "Web3 provider not reachable"

contract_address = Web3.to_checksum_address(os.getenv("CONTRACT_ADDRESS"))
abi = json.loads("""[
    {"anonymous":false,"inputs":[
        {"indexed":true,"internalType":"address","name":"requester","type":"address"},
        {"indexed":true,"internalType":"bytes32","name":"requestId","type":"bytes32"},
        {"indexed":false,"internalType":"string","name":"prompt","type":"string"},
        {"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}
    ],"name":"RequestGenerated","type":"event"}
]""")
contract = w3.eth.contract(address=contract_address, abi=abi)

# In‑memory store of processed request IDs (in prod → DB/Redis)
processed = set()

# -------------------------------------------------
# 2️⃣ AI Model Setup
# -------------------------------------------------
model_name = os.getenv("MODEL_NAME", "gpt2")
generator = pipeline("text-generation", model=model_name, device=0)  # GPU if available
MAX_LENGTH = int(os.getenv("MAX_LENGTH", "64"))

# -------------------------------------------------
# 3️⃣ Helper: Verify that request is legit & not processed
# -------------------------------------------------
def is_valid_event(event) -> bool:
    request_id = event["args"]["requestId"]
    if request_id in processed:
        return False
    # Mark as processed early to avoid race conditions
    processed.add(request_id)
    return True

# -------------------------------------------------
# 4️⃣ Route: Poll for new events (simple polling endpoint)
# -------------------------------------------------
@app.route("/poll", methods=["GET"])
def poll_events():
    # Get latest block number (you could store last seen block in DB)
    latest = w3.eth.block_number
    from_block = max(latest - 5000, 0)  # look back ~1‑day on Sepolia
    events = contract.events.RequestGenerated().get_logs(
        fromBlock=from_block, toBlock=latest
    )
    new_requests = []
    for ev in events:
        if is_valid_event(ev):
            new_requests.append({
                "requester": ev["args"]["requester"],
                "requestId": ev["args"]["requestId"].hex(),
                "prompt": ev["args"]["prompt"],
                "timestamp": ev["args"]["timestamp"]
            })
    return jsonify(new_requests)

# -------------------------------------------------
# 5️⃣ Route: Generate text for a specific requestId
# -------------------------------------------------
@app.route("/generate", methods=["POST"])
def generate():
    data = request.get_json()
    request_id = data.get("requestId")
    prompt = data.get("prompt")
    if not request_id or not prompt:
        return jsonify({"error": "Missing fields"}), 400

    # Simple idempotency guard
    if request_id in processed:
        return jsonify({"error": "Already processed"}), 409

    # Run model
    set_seed(42)   # deterministic for demo; remove for randomness
    output = generator(prompt, max_length=MAX_LENGTH, num_return_sequences=1)[0]["generated_text"]

    # Mark processed
    processed.add(request_id)

    return jsonify({"requestId": request_id, "output": output})

# -------------------------------------------------
# 6️⃣ Health check
# -------------------------------------------------
@app.route("/health", methods=["GET"])
def health():
    return "OK", 200

if __name__ == "__main__":
    # In production use gunicorn or uvicorn (ASGI) behind a reverse proxy
    app.run(host="0.0.0.0", port=5000, debug=True)
Enter fullscreen mode Exit fullscreen mode

How it works

  1. Polling endpoint (/poll) – the frontend calls this every few seconds. It fetches recent RequestGenerated events, filters out already‑handled IDs, and returns an array of pending requests.
  2. Generate endpoint (/generate) – the frontend sends the requestId and prompt it just received. The backend runs the GPT‑2 model and returns the generated text.

Tip: In production you would replace the polling model with a WebSocket or Chainlink Keepers to push events instantly, and store processed IDs in a persistent DB (PostgreSQL, Redis, etc.) to survive restarts.


4️⃣ Frontend – Simple HTML + Ethers.js

4.1 Install Ethers

cd ../frontend
npm init -y
npm install ethers
Enter fullscreen mode Exit fullscreen mode

4.2 index.html


html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Pay‑per‑Use AI</title>
  <style>
    body {font-family: Arial, sans-serif; max-width: 600px; margin: 2rem auto;}
    textarea {width: 100%; height: 120px;}
    button {padding: 0.6rem 1rem; margin-top: 0.5rem;}
    #output {border: 1px solid #

#coding #tutorial #web3 #AI
Enter fullscreen mode Exit fullscreen mode

Top comments (0)