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 Inference Marketplace” – a tiny dApp where users can pay a small amount of Ether (or any ERC‑20 token) to get a prediction from a machine‑learning model (sentiment analysis on a short text).

The tutorial covers everything you need to spin the project up locally, test it, and finally deploy it to a public testnet (Sepolia).

What you’ll learn

  1. Setting up a Solidity smart‑contract that holds a price and records requests.
  2. Training a lightweight PyTorch model and exporting it as a TorchScript file.
  3. Building a Node.js/Express “oracle” server that (a) receives payments, (b) calls the AI model, and (c) returns the result on‑chain via an event.
  4. Writing a simple React front‑end that talks to MetaMask and the oracle.
  5. Deploying the contract with Hardhat, publishing the oracle to a cloud VM (or Railway/Render), and wiring everything together.

📋 Table of Contents

# Section What you’ll get
1 Prerequisites Tools, accounts, basic knowledge
2 Project Layout Directory tree & purpose of each folder
3 Train & Export the AI Model Python script, TorchScript, test inference
4 Write the Solidity Contract Pricing, request tracking, event emission
5 Hardhat Setup & Deployment Compilation, test, Sepolia deployment
6 Oracle Server (Node.js) Express API, Web3 interaction, model inference
7 Front‑End (React + ethers.js) UI, MetaMask integration, request flow
8 End‑to‑End Testing Local testnet, Sepolia test, troubleshooting
9 Production‑Ready Deployment Cloud VM, Docker, environment variables, SSL
10 Next Steps & Security Checklist Scaling, Oracles, DAO governance, audit pointers

1️⃣ Prerequisites

Category Minimum Recommended
Programming JavaScript/TypeScript basics, Python basics Familiarity with async/await and Solidity
Blockchain MetaMask, some test‑ETH on Sepolia (use faucet) Understanding of ERC‑20, events, gas
AI/ML Python 3.9+, pip, basic PyTorch GPU optional (model is tiny)
Dev Tools - Node 20 (or 18 LTS)
- npm or Yarn
- Hardhat (npm i -D hardhat)
- Python 3.9+
- Git
Docker (for production)
Accounts - GitHub (optional)
- Infura/Alchemy project (Sepolia RPC)
- Cloud provider (Railway, Render, or any VPS)
- Cloudflare DNS (optional)
OS macOS / Linux / WSL2 (Windows) Any

Quick Install (macOS / Linux)

# Node & npm
brew install node          # macOS (or apt-get install nodejs npm)

# Hardhat (global optional)
npm i -g hardhat

# Python & venv
python3 -m venv .venv && source .venv/bin/activate
pip install torch torchvision torchaudio tqdm

# Git
brew install git
Enter fullscreen mode Exit fullscreen mode

Make sure you have MetaMask installed in your browser and set the network to Sepolia.


2️⃣ Project Layout

crypto‑ai‑marketplace/
├─ contracts/               # Solidity contracts
│   └─ InferenceMarketplace.sol
├─ scripts/                 # Hardhat deployment scripts
│   └─ deploy.js
├─ test/                    # Hardhat tests (JS/TS)
│   └─ InferenceMarketplace.test.js
├─ ai/                      # Python AI code
│   ├─ train.py
│   ├─ model.pt             # exported TorchScript model (generated)
│   └─ requirements.txt
├─ server/                  # Node.js oracle
│   ├─ index.js
│   ├─ package.json
│   └─ .env                 # secrets (Infura key, contract address, etc.)
├─ client/                  # React front‑end
│   ├─ src/
│   │   ├─ App.jsx
│   │   └─ components/
│   └─ package.json
├─ hardhat.config.js
└─ README.md
Enter fullscreen mode Exit fullscreen mode

We’ll fill each folder step‑by‑step.


3️⃣ Train & Export the AI Model

3.1 Choose a tiny model

We’ll use DistilBERT (a 66 M parameter transformer) fine‑tuned for binary sentiment classification. The model is small enough to run on a CPU in < 50 ms.

3.2 Install Python deps

ai/requirements.txt

torch==2.2.0
transformers==4.38.0
datasets==2.16.0
tqdm==4.66.2
Enter fullscreen mode Exit fullscreen mode
cd ai
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

3.3 Training script

ai/train.py

import torch
from torch import nn
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from datasets import load_dataset
from tqdm.auto import tqdm

MODEL_NAME = "distilbert-base-uncased"
DATASET = "imdb"               # binary sentiment dataset
EPOCHS = 1                     # for demo; increase for real accuracy
BATCH_SIZE = 16
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def main():
    # 1️⃣ Load dataset (train/validation split)
    raw = load_dataset("imdb")
    train_ds = raw["train"].shuffle(seed=42).select(range(2000))  # tiny subset
    val_ds   = raw["test"].shuffle(seed=42).select(range(500))

    # 2️⃣ Tokenizer
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

    def tokenize(batch):
        return tokenizer(batch["text"], padding="max_length", truncation=True, max_length=128)

    train_enc = train_ds.map(tokenize, batched=True, batch_size=BATCH_SIZE)
    val_enc   = val_ds.map(tokenize, batched=True, batch_size=BATCH_SIZE)

    # 3️⃣ Convert to torch tensors
    columns = ["input_ids", "attention_mask", "label"]
    train_enc.set_format(type="torch", columns=columns)
    val_enc.set_format(type="torch", columns=columns)

    # 4️⃣ Model
    model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2)
    model.to(DEVICE)

    optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
    loss_fn = nn.CrossEntropyLoss()

    # 5️⃣ Training loop
    model.train()
    for epoch in range(EPOCHS):
        prog = tqdm(train_enc, total=len(train_enc), desc=f"Epoch {epoch+1}")
        for batch in prog:
            optimizer.zero_grad()
            input_ids = batch["input_ids"].to(DEVICE)
            attn_mask = batch["attention_mask"].to(DEVICE)
            labels    = batch["label"].to(DEVICE)

            outputs = model(input_ids, attention_mask=attn_mask, labels=labels)
            loss = outputs.loss
            loss.backward()
            optimizer.step()
            prog.set_postfix(loss=loss.item())

    # 6️⃣ Save as TorchScript (self‑contained, no Python needed)
    model.eval()
    example = {
        "input_ids": torch.randint(0, 1000, (1, 128), dtype=torch.long).to(DEVICE),
        "attention_mask": torch.ones((1, 128), dtype=torch.long).to(DEVICE)
    }
    scripted = torch.jit.trace(
        lambda **kwargs: model(**kwargs).logits,
        example_kwarg_inputs=example
    )
    scripted.save("model.pt")
    print("✅ Model exported to model.pt")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Why TorchScript?

The Node.js oracle will load the model with torchscript-node (a thin binding) or via onnxruntime-node. TorchScript produces a single binary file that can be loaded without Python, making the inference server lightweight.

3.4 Run training (once)

cd ai
python train.py
Enter fullscreen mode Exit fullscreen mode

You’ll get model.pt (≈ 250 MB). Commit it to the repo (or store on IPFS if you want a truly decentralized model). For the tutorial we keep it local.

3.5 Quick inference test

import torch

model = torch.jit.load("model.pt")
model.eval()

def predict(text: str):
    from transformers import AutoTokenizer
    tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
    enc = tokenizer(text, truncation=True, padding="max_length", max_length=128, return_tensors="pt")
    with torch.no_grad():
        logits = model(**enc)
    prob = torch.softmax(logits, dim=-1).squeeze()
    label = "POSITIVE" if prob[1] > prob[0] else "NEGATIVE"
    return label, prob.tolist()

print(predict("I love crypto!"))
Enter fullscreen mode Exit fullscreen mode

You should see a POSITIVE label with a probability vector.


4️⃣ Solidity Smart Contract

4.1 Core requirements

  • price – how much the user pays per inference (in wei).
  • request mapping – tracks which address requested which text (hash).
  • eventInferenceRequested(address indexed user, bytes32 requestId, string text)
  • functionrequestInference(string calldata text) payable, checks price, emits event.
  • owner – can update price.

4.2 Contract code

contracts/InferenceMarketplace.sol

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

/// @title Crypto‑AI Inference Marketplace
/// @notice Users pay a fixed price to request a prediction from an off‑chain AI model.
/// @dev The contract only stores request metadata and emits an event. The off‑chain oracle
///      listens for the event, runs inference, then calls `fulfillInference`.
contract InferenceMarketplace {
    address public owner;
    uint256 public price; // price per inference (in wei)

    // requestId => result (0 = pending, 1 = positive, 2 = negative)
    mapping(bytes32 => uint8) public results;

    event InferenceRequested(address indexed user, bytes32 indexed requestId, string text);
    event InferenceFulfilled(address indexed user, bytes32 indexed requestId, uint8 result);

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

    constructor(uint256 _price) {
        owner = msg.sender;
        price = _price;
    }

    /// @notice User sends `price` wei + text to get a prediction.
    function requestInference(string calldata text) external payable returns (bytes32 requestId) {
        require(msg.value == price, "Incorrect payment");
        requestId = keccak256(abi.encodePacked(msg.sender, block.timestamp, text));
        // Store a placeholder (0 = pending)
        results[requestId] = 0;
        emit InferenceRequested(msg.sender, requestId, text);
    }

    /// @notice Oracle calls this after computing the result.
    /// @param requestId The id emitted in the request event.
    /// @param result 1 = POSITIVE, 2 = NEGATIVE
    function fulfillInference(bytes32 requestId, uint8 result) external onlyOwner {
        require(results[requestId] == 0, "Already fulfilled");
        require(result == 1 || result == 2, "Invalid result");
        results[requestId] = result;
        // Find the original user from the requestId (cannot be derived on‑chain,
        // so we emit the user address again for front‑ends to catch)
        emit InferenceFulfilled(msg.sender, requestId, result);
    }

    /// @notice Owner can withdraw accumulated fees.
    function withdraw() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    /// @notice Owner can change the price.
    function setPrice(uint256 _price) external onlyOwner {
        price = _price;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why store the result on‑chain?

  • Guarantees immutability – the user can verify the oracle did not tamper with the answer.
  • Front‑ends can simply read results[requestId] to display the outcome.

5️⃣ Hardhat Setup & Deployment

5.1 Initialise Hardhat

mkdir crypto-ai-marketplace && cd crypto-ai-marketplace
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

Replace the auto‑generated sample contract with InferenceMarketplace.sol.

5.2 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, // e.g., https://sepolia.infura.io/v3/<KEY>
      accounts: [process.env.PRIVATE_KEY], // deployer account
    },
    localhost: {
      url: "http://127.0.0.1:8545",
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Create a .env file (never commit it):

SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/your-infura-id
PRIVATE_KEY=0xYOUR_PRIVATE_KEY   # account with Sepolia test ETH
Enter fullscreen mode Exit fullscreen mode

5.3 Deploy script

script/deploy.js

const { ethers } = require("hardhat");

async function main() {
  const price = ethers.parseEther("0.001"); // 0.001 ETH per inference
  const InferenceMarketplace = await ethers.getContractFactory("InferenceMarketplace");
  const contract = await InferenceMarketplace.deploy(price);
  await contract.waitForDeployment();

  console.log("Contract deployed to:", contract.target);
  console.log("Price (wei):", price.toString());
}

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

Run locally on Hardhat network first:

npx hardhat node   # in a separate terminal
npx hardhat run script/deploy.js --network localhost
Enter fullscreen mode Exit fullscreen mode

You’ll see the contract address (e.g., 0xAbc...). Keep it – the oracle and front‑end need it.

5.4 Deploy to Sepolia

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

Copy the printed address; later we’ll put it in server/.env and client/.env.

5.5 Test (optional)

test/InferenceMarketplace.test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("InferenceMarketplace", function () {
  let contract, owner, user;
  const price = ethers.parseEther("0.001");

  beforeEach(async () => {
    [owner, user] = await ethers.getSigners();
    const Factory = await ethers.getContractFactory("InferenceMarketplace");
    contract = await Factory.deploy(price);
    await contract.waitForDeployment();
  });

  it("rejects wrong payment", async () => {
    await expect(contract.connect(user).requestInference("test", { value: ethers.parseEther("0.0005") }))
      .to.be.revertedWith("Incorrect payment");
  });

  it("accepts correct payment and emits event", async () => {
    await expect(contract.connect(user).requestInference("hello", { value: price }))
      .to.emit(contract, "InferenceRequested")
      .withArgs(user.address, anyValue, "hello");
  });
});
Enter fullscreen mode Exit fullscreen mode

Run:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

All green → you’re ready for the oracle.


6️⃣ Oracle Server (Node.js)

The oracle does three things:

  1. Listen to InferenceRequested events (via ethers.js + WebSocket provider).
  2. Run the TorchScript model on the supplied text.
  3. Call fulfillInference on the contract with the result (signed by the owner key).

6.1 Initialise the server

mkdir server && cd server
npm init -y
npm i express ethers dotenv @xenova/transformers   # we’ll use @xenova/transformers for on‑node inference (no Python)
npm i -D nodemon
Enter fullscreen mode Exit fullscreen mode

Alternative: Use torchscript-node bindings (requires native build). For simplicity and cross‑platform compatibility we’ll use the JavaScript inference library that can load the ONNX version of the model (exported later).

6.2 Export ONNX from PyTorch (once)

Add to ai/train.py after saving model.pt:


python
torch.onnx.export(
    model,
    (example["input_ids"], example["attention_mask"]),
    "model.onnx",
    input_names=["input_ids", "attention_mask"],
    output_names=["logits"],
    dynamic_axes={"input_ids": {0:

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

Top comments (0)