DEV Community

Naren karthi
Naren karthi

Posted on

How to Build an Autonomous Trading Agent with Python

Crypto‑AI Project Tutorial

Project: Decentralized AI‑Inference Marketplace – users can request predictions from an on‑chain AI model. A Solidity smart‑contract mints a Prediction‑Token (PRED) that pays the AI service for each inference. The AI service runs a TensorFlow model behind a Flask API, verifies the payment on‑chain, and returns the result.

What you’ll build

  1. Smart contract – handles token minting, request/pay‑out logic, and stores request metadata.
  2. AI inference server – a Flask app that loads a tiny TensorFlow model (MNIST digit recognizer) and serves predictions via HTTP.
  3. Front‑end dApp – React + ethers.js UI that lets a user (a) connect a wallet, (b) fund the contract, (c) upload an image, (d) pay for a prediction, and (e) display the result.
  4. Deployment – contract to an Ethereum testnet (Sepolia), AI server on Docker (or a cloud VM), dApp on Vercel/Netlify.

1️⃣ Prerequisites

Area Required tools / versions Why
General Git, Node.js ≥ 18, npm ≥ 9, Python ≥ 3.10, Docker ≥ 24 Version control, package management, runtime, containerisation
Blockchain Hardhat ≥ 2.20, ethers ≥ 6, MetaMask extension, Sepolia testnet faucet Compile, test, and deploy Solidity contracts
AI TensorFlow ≥ 2.13, Pillow, Flask ≥ 2.3, gunicorn (optional) Model loading & serving
Front‑end React ≥ 18, Vite, ethers, wagmi, TailwindCSS (optional) UI + Web3 integration
Optional VS Code, Postman/Insomnia (API testing), GitHub Actions (CI) Productivity & automation

Hardware note: Anything that can run Docker (≥ 4 GB RAM) is fine – even a laptop.


2️⃣ Project Layout

crypto‑ai‑marketplace/
├─ contracts/                # Solidity contracts
│   └─ PredictionMarketplace.sol
├─ ai-service/               # Python Flask + TensorFlow
│   ├─ model/                # Saved model (mnist)
│   ├─ app.py
│   └─ requirements.txt
├─ dapp/                     # React front‑end
│   ├─ src/
│   │   ├─ components/
│   │   └─ hooks/
│   └─ vite.config.ts
├─ scripts/                  # Hardhat deployment scripts
│   └─ deploy.ts
├─ .env                      # Secrets (Infura/Alchemy key, private key)
└─ README.md
Enter fullscreen mode Exit fullscreen mode

3️⃣ Step‑by‑Step Implementation

3.1 Initialise the repo

mkdir crypto-ai-marketplace && cd crypto-ai-marketplace
git init
Enter fullscreen mode Exit fullscreen mode

3.2 Set up the Solidity contract

3.2.1 Install Hardhat

npm init -y
npm i -D hardhat
npx hardhat   # choose "Create a basic sample project"
Enter fullscreen mode Exit fullscreen mode

Delete the sample contract (contracts/Greeter.sol) and create contracts/PredictionMarketplace.sol:

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

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

contract PredictionToken is ERC20 {
    constructor() ERC20("Prediction Token", "PRED") {
        _mint(msg.sender, 1_000_000 * 10 ** decimals()); // initial supply to deployer
    }
}

/**
 * @title PredictionMarketplace
 * @notice Users pay PRED tokens to get an AI inference. The contract holds
 *         the model price and forwards it to the off‑chain AI service.
 */
contract PredictionMarketplace {
    PredictionToken public immutable token;
    address public immutable aiService; // off‑chain service address (for auth)

    // Cost per prediction (in wei of PRED)
    uint256 public pricePerPrediction = 10 * 10 ** 18; // 10 PRED

    // requestId => requester
    mapping(uint256 => address) public requesters;
    uint256 public nextRequestId;

    event PredictionRequested(uint256 indexed requestId, address indexed requester, bytes data);
    event PredictionFulfilled(uint256 indexed requestId, address indexed requester, string result);

    constructor(address _token, address _aiService) {
        token = PredictionToken(_token);
        aiService = _aiService;
    }

    /**
     * @dev User approves `pricePerPrediction` tokens to this contract first.
     *      Then calls this function to lock the payment and emit an event.
     */
    function requestPrediction(bytes calldata data) external {
        require(token.transferFrom(msg.sender, address(this), pricePerPrediction),
                "Token transfer failed");

        uint256 requestId = nextRequestId++;
        requesters[requestId] = msg.sender;

        emit PredictionRequested(requestId, msg.sender, data);
    }

    /**
     * @dev Called by the off‑chain AI service (via a signed tx) to deliver result.
     *      In production you’d use an oracle or a verify‑signature scheme.
     */
    function fulfillPrediction(uint256 requestId, string calldata result) external {
        require(msg.sender == aiService, "Only AI service can fulfill");
        address requester = requesters[requestId];
        require(requester != address(0), "Invalid request");

        // forward payment to AI service (or split, etc.)
        token.transfer(aiService, pricePerPrediction);

        emit PredictionFulfilled(requestId, requester, result);
        delete requesters[requestId];
    }

    /** Owner can change price */
    function setPrice(uint256 _price) external {
        // simple access control for demo – replace with Ownable in prod
        require(msg.sender == address(token), "Only token owner");
        pricePerPrediction = _price;
    }
}
Enter fullscreen mode Exit fullscreen mode

3.2.2 Install OpenZeppelin contracts

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

3.2.3 Compile & test

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Create a quick test (test/PredictionMarketplace.js) to sanity‑check the flow (omitted for brevity – you can copy from Hardhat sample and adjust).

3.3 Deploy the contracts to Sepolia

  1. Create .env (never commit!).
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/<INFURA_PROJECT_ID>
PRIVATE_KEY=0xYOUR_PRIVATE_KEY   # account with Sepolia test ETH
Enter fullscreen mode Exit fullscreen mode
  1. Add deployment script scripts/deploy.ts (TypeScript).
import { ethers } from "hardhat";
import * as dotenv from "dotenv";
dotenv.config();

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

  // Deploy PredictionToken first
  const TokenFactory = await ethers.getContractFactory("PredictionToken");
  const token = await TokenFactory.deploy();
  await token.waitForDeployment();
  console.log("PredictionToken deployed to:", token.target);

  // Deploy PredictionMarketplace
  const MarketplaceFactory = await ethers.getContractFactory("PredictionMarketplace");
  // For demo we use the same deployer as the AI service address
  const marketplace = await MarketplaceFactory.deploy(token.target, deployer.address);
  await marketplace.waitForDeployment();
  console.log("PredictionMarketplace deployed to:", marketplace.target);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode
  1. Deploy
npx hardhat run scripts/deploy.ts --network sepolia
Enter fullscreen mode Exit fullscreen mode

Copy the two deployed addresses – you’ll need them for the AI service and dApp.


4️⃣ AI Inference Service (Flask + TensorFlow)

4.1 Prepare the model

We'll use a pre‑trained MNIST digit classifier (tiny ~ 1 MB).

mkdir -p ai-service/model && cd ai-service/model
python - <<'PY'
import tensorflow as tf, pathlib, os
# Load MNIST from tf.keras.datasets
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train[..., tf.newaxis]/255.0
x_test = x_test[..., tf.newaxis]/255.0

model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32,3,activation='relu',input_shape=(28,28,1)),
    tf.keras.layers.MaxPooling2D(),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128,activation='relu'),
    tf.keras.layers.Dense(10,activation='softmax')
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.fit(x_train, y_train, epochs=1, validation_split=0.1)  # quick train
model.save('mnist')
print("Saved model to", pathlib.Path('mnist').absolute())
PY
Enter fullscreen mode Exit fullscreen mode

4.2 Flask API

Create ai-service/app.py:

# app.py
import os, json, base64, hashlib, hmac
from flask import Flask, request, jsonify
import tensorflow as tf
from PIL import Image
import numpy as np

app = Flask(__name__)

# Load model once at startup
model = tf.keras.models.load_model(os.path.join('model', 'mnist'))

# -------------------------------------------------
# Helper: decode base64 image from dApp, resize to 28x28
# -------------------------------------------------
def preprocess_image(b64_str: str) -> np.ndarray:
    img_data = base64.b64decode(b64_str.split(',')[1])  # strip data:image/...;base64,
    img = Image.open(io.BytesIO(img_data)).convert('L')  # grayscale
    img = img.resize((28, 28))
    arr = np.array(img) / 255.0
    return arr.reshape(1, 28, 28, 1)

# -------------------------------------------------
# Config – set these to match your on‑chain addresses
# -------------------------------------------------
CONTRACT_ADDRESS = os.getenv('CONTRACT_ADDRESS')   # PredictionMarketplace address
AI_SERVICE_PRIVATE_KEY = os.getenv('AI_SERVICE_PRIVATE_KEY')  # for signing txs
WEB3_PROVIDER = os.getenv('WEB3_PROVIDER')       # e.g. https://sepolia.infura.io/v3/...

# -------------------------------------------------
# Route: health check
# -------------------------------------------------
@app.get("/health")
def health():
    return {"status": "ok"}

# -------------------------------------------------
# Route: receive prediction request (off‑chain listener)
# -------------------------------------------------
@app.post("/predict")
def predict():
    """
    Expected JSON payload:
    {
        "requestId": "123",
        "data": "<base64 png/jpg>",
        "requester": "0xABC..."
    }
    """
    payload = request.get_json()
    request_id = int(payload["requestId"])
    b64_image = payload["data"]
    requester = payload["requester"]

    # 1️⃣ Preprocess
    img = preprocess_image(b64_image)

    # 2️⃣ Predict
    probs = model.predict(img)[0]
    digit = int(np.argmax(probs))

    # 3️⃣ Send tx back to contract (fulfillPrediction)
    # Using web3.py for simplicity
    from web3 import Web3
    w3 = Web3(Web3.HTTPProvider(WEB3_PROVIDER))
    with open('abi/PredictionMarketplace.json') as f:
        abi = json.load(f)

    contract = w3.eth.contract(address=CONTRACT_ADDRESS, abi=abi)
    nonce = w3.eth.get_transaction_count(w3.to_checksum_address(w3.eth.account.from_key(AI_SERVICE_PRIVATE_KEY).address))

    tx = contract.functions.fulfillPrediction(request_id, str(digit)).build_transaction({
        'chainId': 11155111,          # Sepolia chain ID
        'gas': 200_000,
        'gasPrice': w3.to_wei('10', 'gwei'),
        'nonce': nonce,
    })
    signed_tx = w3.eth.account.sign_transaction(tx, AI_SERVICE_PRIVATE_KEY)
    tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

    return {
        "requestId": request_id,
        "prediction": digit,
        "txHash": tx_hash.hex(),
        "txStatus": receipt.status
    }

if __name__ == "__main__":
    # run with gunicorn for production: `gunicorn -w 4 app:app`
    app.run(host="0.0.0.0", port=8000)
Enter fullscreen mode Exit fullscreen mode

Key points

What Why
Base64 image Easy to embed in JSON from the browser
fulfillPrediction tx The AI service proves it performed the inference by sending a signed transaction back to the contract. In a production system you’d replace this with a trusted oracle (Chainlink) or a signature‑verification pattern to avoid giving the AI service a private key that can move funds.
Environment variables Keep secrets out of source. You’ll inject them via Docker compose.

4.3 Dockerise the AI service

Create ai-service/Dockerfile:

# Base image
FROM python:3.11-slim

WORKDIR /app

# Install system deps (Pillow needs libjpeg)
RUN apt-get update && apt-get install -y libjpeg62-turbo && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# expose port 8000
EXPOSE 8000

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]
Enter fullscreen mode Exit fullscreen mode

requirements.txt:

flask
tensorflow==2.13.*
pillow
numpy
web3
gunicorn
Enter fullscreen mode Exit fullscreen mode

Build & run locally

cd ai-service
docker build -t ai-service .
docker run -d -p 8000:8000 \
  -e CONTRACT_ADDRESS=0xYourMarketplace \
  -e AI_SERVICE_PRIVATE_KEY=0xYourPrivateKey \
  -e WEB3_PROVIDER=$SEPOLIA_RPC_URL \
  ai-service
Enter fullscreen mode Exit fullscreen mode

Test with Postman:

GET http://localhost:8000/health   → {"status":"ok"}
Enter fullscreen mode Exit fullscreen mode

5️⃣ Front‑End dApp (React + Vite)

5.1 Scaffold

cd ..
npm create vite@latest dapp --template react-ts
cd dapp
npm i ethers wagmi @wagmi/core @rainbow-me/rainbowkit tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Configure Tailwind (tailwind.config.cjs) – minimal.

5.2 Connect wallet (wagmi + RainbowKit)

Create src/wallet.ts:

import { createConfig, http } from '@wagmi/core';
import { sepolia } from '@wagmi/core/chains';
import { injectedConnector } from 'wagmi/connectors/injected';
import { createWalletClient, custom } from 'viem';
import { metaMask } from '@wagmi/connectors';

export const config = createConfig({
  chains: [sepolia],
  transports: {
    [sepolia.id]: http(import.meta.env.VITE_SEPOLIA_RPC_URL),
  },
  connectors: [
    new injectedConnector({ chains: [sepolia] })
  ],
  client: createWalletClient({
    chain: sepolia,
    transport: custom(window.ethereum as any)
  })
});
Enter fullscreen mode Exit fullscreen mode

Add RainbowKit in src/main.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { WagmiConfig } from 'wagmi';
import { RainbowKitProvider, getDefaultWallets } from '@rainbow-me/rainbowkit';
import { config } from './wallet';
import '@rainbow-me/rainbowkit/styles.css';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <WagmiConfig config={config}>
      <RainbowKitProvider>
        <App />
      </RainbowKitProvider>
    </WagmiConfig>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

5.3 Contract ABIs

Copy the compiled ABIs from artifacts/contracts/PredictionToken.sol/PredictionToken.json and PredictionMarketplace.sol/PredictionMarketplace.json into src/abi/.

5.4 Core UI (src/App.tsx)


tsx
import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useState, ChangeEvent } from 'react';
import { ethers } from 'ethers';
import tokenAbi from './abi/PredictionToken.json';
import marketAbi from './abi/PredictionMarketplace.json';

const TOKEN_ADDRESS = import.meta.env.VITE_TOKEN_ADDRESS;          // deployed token
const MARKET_ADDRESS = import.meta.env.VITE_MARKET_ADDRESS;        // deployed marketplace
const AI_SERVICE_URL = import.meta.env.VITE_AI_SERVICE_URL;        // e.g. http://localhost:8000

function App() {
  const { address, isConnected } = useAccount();
  const [file, setFile] = useState<File | null>(null);
  const [prediction, setPrediction] = useState<string | null>(null);
  const [

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

Top comments (0)