DEV Community

Naren karthi
Naren karthi

Posted on

How to Build an Autonomous Trading Agent with Python

🚀 End‑to‑End Tutorial

Project: AI‑Powered Crypto Signal dApp

A minimal, production‑ready example that shows how to combine a machine‑learning model (sentiment analysis of crypto news) with a blockchain smart contract that stores signals and rewards token holders.

You’ll end up with three moving parts:

Part Tech What it does
AI service Python + FastAPI + 🤗 Transformers Pulls the latest crypto news, runs a sentiment model, returns a “BUY/SELL/HOLD” signal via a REST endpoint.
Smart contract Solidity + Hardhat ERC‑20 “SignalToken” (SGT) that mints rewards when a new signal is posted and lets holders claim them.
Frontend React + Ethers.js UI to request a new signal, view the last signal, and claim rewards.

All three pieces can be deployed for free on test‑networks (Sepolia, Polygon Mumbai) and public cloud services (Render, Vercel, Netlify).


📋 Table of Contents

  1. Prerequisites
  2. Folder Structure
  3. 1️⃣ Build the AI Service
  4. 2️⃣ Write & Test the Solidity Contract
  5. 3️⃣ Front‑end React App
  6. 4️⃣ Deploy Everything
  7. Next Steps & Security Tips

1️⃣ Prerequisites

Category Tool Version (tested) Install Command
Node Node.js v20.x brew install node / nvm install 20
npm 10.x bundled with Node
Hardhat npm i -D hardhat
Python Python 3.10+ brew install python@3.10
pip bundled
FastAPI 0.115 pip install fastapi uvicorn[standard]
Transformers 4.44 pip install transformers torch
Requests 2.32 pip install requests
Blockchain Metamask (browser extension) Chrome/Firefox add‑on
Sepolia test‑ETH faucet https://sepoliafaucet.com/
IDE VS Code (recommended) https://code.visualstudio.com/
Optional (Docker) Docker Desktop latest https://www.docker.com/products/docker-desktop/

Environment variables

Create a .env file in the root of the repo (add it to .gitignore).

# ---------- AI SERVICE ----------
NEWS_API_KEY=YOUR_NEWSAPI_OR_ALTERNATIVE_KEY
HUGGINGFACE_TOKEN=hf_XXXXXXXXXXXXXXXXXXXX   # optional, for private models

# ---------- BLOCKCHAIN ----------
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
PRIVATE_KEY=0xYOUR_WALLET_PRIVATE_KEY   # used by Hardhat deploy script

# ---------- FRONTEND ----------
REACT_APP_BACKEND_URL=http://localhost:8000   # will be overridden in prod
Enter fullscreen mode Exit fullscreen mode

2️⃣ Folder Structure

crypto‑ai‑dapp/
│
├─ backend/               # Python FastAPI service
│   ├─ app/
│   │   ├─ __init__.py
│   │   ├─ main.py
│   │   └─ model.py
│   ├─ requirements.txt
│   └─ Dockerfile
│
├─ contracts/             # Solidity contracts + Hardhat
│   ├─ contracts/
│   │   └─ SignalToken.sol
│   ├─ test/
│   │   └─ SignalToken.test.js
│   ├─ scripts/
│   │   └─ deploy.js
│   ├─ hardhat.config.js
│   └─ package.json
│
├─ frontend/              # React app
│   ├─ src/
│   │   ├─ components/
│   │   │   ├─ SignalCard.jsx
│   │   │   └─ ClaimButton.jsx
│   │   ├─ App.jsx
│   │   └─ index.jsx
│   ├─ public/
│   ├─ vite.config.js
│   └─ package.json
│
└─ .env
Enter fullscreen mode Exit fullscreen mode

3️⃣ Build the AI Service

3.1 Install dependencies

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

backend/requirements.txt

fastapi
uvicorn[standard]
transformers
torch
requests
python-dotenv
Enter fullscreen mode Exit fullscreen mode

3.2 Model wrapper (model.py)

# backend/app/model.py
import os
from transformers import pipeline, AutoModelForSequenceClassification, AutoTokenizer
from dotenv import load_dotenv

load_dotenv()

# Use a small, fast sentiment model (distilbert-base-uncased-finetuned-sst-2)
MODEL_NAME = "distilbert-base-uncased-finetuned-sst-2-english"

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
sentiment = pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)

def analyse_text(text: str) -> str:
    """Return BUY/SELL/HOLD based on sentiment score."""
    result = sentiment(text)[0]      # e.g. {'label': 'POSITIVE', 'score': 0.997}
    label, score = result["label"], result["score"]
    # Simple heuristic:
    if label == "POSITIVE" and score > 0.8:
        return "BUY"
    elif label == "NEGATIVE" and score > 0.8:
        return "SELL"
    else:
        return "HOLD"
Enter fullscreen mode Exit fullscreen mode

3.3 News fetcher & API (main.py)

# backend/app/main.py
import os
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from dotenv import load_dotenv
from .model import analyse_text

load_dotenv()
NEWS_API_KEY = os.getenv("NEWS_API_KEY")
if not NEWS_API_KEY:
    raise RuntimeError("NEWS_API_KEY missing in .env")

app = FastAPI(title="Crypto Sentiment Service")

class SignalResponse(BaseModel):
    signal: str          # BUY / SELL / HOLD
    headline: str
    source: str
    url: str

def fetch_latest_crypto_news() -> dict:
    """Pull the most recent crypto news headline."""
    url = "https://newsapi.org/v2/everything"
    params = {
        "q": "cryptocurrency OR bitcoin OR ethereum",
        "language": "en",
        "sortBy": "publishedAt",
        "pageSize": 1,
        "apiKey": NEWS_API_KEY,
    }
    resp = requests.get(url, params=params)
    data = resp.json()
    if resp.status_code != 200 or not data.get("articles"):
        raise HTTPException(status_code=502, detail="News API failure")
    article = data["articles"][0]
    return article

@app.get("/signal", response_model=SignalResponse)
def get_signal():
    article = fetch_latest_crypto_news()
    headline = article["title"]
    signal = analyse_text(headline)
    return SignalResponse(
        signal=signal,
        headline=headline,
        source=article["source"]["name"],
        url=article["url"],
    )
Enter fullscreen mode Exit fullscreen mode

3.4 Run locally

uvicorn app.main:app --host 0.0.0.0 --port 8000
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:8000/signal → you’ll see a JSON payload like:

{
  "signal": "BUY",
  "headline": "Bitcoin rallies past $30,000 as institutional interest surges",
  "source": "CoinDesk",
  "url": "https://www.coindesk.com/..."
}
Enter fullscreen mode Exit fullscreen mode

3.5 Dockerize (optional)

backend/Dockerfile

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

Build & run:

docker build -t crypto-ai-backend .
docker run -d -p 8000:8000 --env-file .env crypto-ai-backend
Enter fullscreen mode Exit fullscreen mode

4️⃣ Write & Test the Solidity Contract

4.1 Initialize Hardhat

cd contracts
npm init -y
npm i -D hardhat @nomicfoundation/hardhat-toolbox ethers dotenv
npx hardhat
# Choose "Create a basic sample project"
Enter fullscreen mode Exit fullscreen mode

4.2 Install OpenZeppelin

npm i @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

4.3 SignalToken.sol

// contracts/SignalToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SignalToken is ERC20 {
    address public admin;          // contract owner (deployer)
    uint256 public rewardPerSignal = 10 * 10**18; // 10 SGT per signal

    // Store the latest signal for UI convenience
    enum Signal { NONE, BUY, SELL, HOLD }
    Signal public lastSignal;
    uint256 public lastSignalBlock;

    // Events
    event SignalPosted(Signal signal, uint256 blockNumber);
    event RewardClaimed(address indexed user, uint256 amount);

    constructor() ERC20("SignalToken", "SGT") {
        admin = msg.sender;
        _mint(admin, 1_000_000 * 10**18); // pre‑mint for rewards
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }

    // --------- Admin functions ----------
    function setReward(uint256 _newReward) external onlyAdmin {
        rewardPerSignal = _newReward;
    }

    // --------- Core logic ----------
    /**
     * @dev Called by the backend (via a signed tx) to register a new signal.
     * The backend must sign the tx with the admin private key.
     */
    function postSignal(string calldata _signal) external onlyAdmin {
        Signal s;
        if (keccak256(bytes(_signal)) == keccak256("BUY")) {
            s = Signal.BUY;
        } else if (keccak256(bytes(_signal)) == keccak256("SELL")) {
            s = Signal.SELL;
        } else {
            s = Signal.HOLD;
        }
        lastSignal = s;
        lastSignalBlock = block.number;
        emit SignalPosted(s, block.number);

        // Mint reward to the contract itself (kept for distribution)
        _mint(address(this), rewardPerSignal);
    }

    /**
     * @dev Anyone holding SGT can claim the reward that accumulated
     * since the last claim. Simple proportional distribution.
     */
    function claimReward() external {
        uint256 totalSupply_ = totalSupply() - balanceOf(address(this));
        require(totalSupply_ > 0, "No tokens in circulation");

        uint256 contractBal = balanceOf(address(this));
        require(contractBal > 0, "No rewards to claim");

        // Pro‑rata share
        uint256 userShare = (contractBal * balanceOf(msg.sender)) / totalSupply_;
        require(userShare > 0, "No reward for you yet");

        // Transfer reward
        _transfer(address(this), msg.sender, userShare);
        emit RewardClaimed(msg.sender, userShare);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why the contract works this way

Feature Reason
Admin‑only postSignal Prevents anyone from spamming the contract; the backend signs the tx using the same admin private key used for deployment.
Reward pool in the contract Keeps rewards inside the contract, making claimReward a pure math operation (no external oracle needed).
Pro‑rata distribution Fairly rewards token holders based on their share of the total supply.

4.4 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],
    },
    // Add Polygon Mumbai if you like:
    // mumbai: { url: process.env.MUMBAI_RPC_URL, accounts: [process.env.PRIVATE_KEY] },
  },
};
Enter fullscreen mode Exit fullscreen mode

4.5 Test (SignalToken.test.js)

// test/SignalToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("SignalToken", function () {
  let SignalToken, token, admin, user1, user2;

  beforeEach(async () => {
    [admin, user1, user2] = await ethers.getSigners();
    SignalToken = await ethers.getContractFactory("SignalToken");
    token = await SignalToken.deploy();
    await token.waitForDeployment();
  });

  it("should mint initial supply to admin", async () => {
    const adminBal = await token.balanceOf(admin.address);
    expect(adminBal).to.equal(ethers.parseEther("1000000"));
  });

  it("admin can post a signal and reward pool grows", async () => {
    await token.postSignal("BUY");
    const pool = await token.balanceOf(token.getAddress());
    expect(pool).to.equal(await token.rewardPerSignal());
  });

  it("users can claim proportional rewards", async () => {
    // admin transfers tokens to users
    await token.transfer(user1.address, ethers.parseEther("100"));
    await token.transfer(user2.address, ethers.parseEther("300"));
    // post a signal (adds reward)
    await token.postSignal("SELL");
    // user1 claims
    await token.connect(user1).claimReward();
    const bal1 = await token.balanceOf(user1.address);
    // Reward pool = 10 SGT; user1 owns 25% (100/400)
    expect(bal1).to.equal(ethers.parseEther("100").add(ethers.parseEther("2.5")));
  });
});
Enter fullscreen mode Exit fullscreen mode

Run tests:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

All should pass.

4.6 Deploy script (deploy.js)

// scripts/deploy.js
async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with:", deployer.address);

  const SignalToken = await ethers.getContractFactory("SignalToken");
  const token = await SignalToken.deploy();

  await token.waitForDeployment();
  console.log("SignalToken deployed at:", await token.getAddress());
}

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

Deploy to Sepolia:

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

Copy the resulting contract address – you’ll need it in the frontend.


5️⃣ Front‑end React App

We’ll use Vite (fast) + Ethers.js for wallet interaction.

5.1 Scaffold

cd frontend
npm create vite@latest . --template react
npm i ethers axios
Enter fullscreen mode Exit fullscreen mode

Add dotenv for Vite env variables (npm i -D vite-plugin-env-compatible optional).

5.2 Vite env variables

Create frontend/.env (Vite prefixes env vars with VITE_):

VITE_BACKEND_URL=http://localhost:8000
VITE_CONTRACT_ADDRESS=0xYourDeployedSignalToken
VITE_SEPOLIA_RPC=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
Enter fullscreen mode Exit fullscreen mode

5.3 App.jsx


jsx
// src/App.jsx
import { useEffect, useState } from "react";
import { ethers } from "ethers";
import axios from "axios";
import SignalCard from "./components/SignalCard";
import ClaimButton from "./components/ClaimButton";

const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
const CONTRACT

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

Top comments (0)