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
- Smart contract – handles token minting, request/pay‑out logic, and stores request metadata.
- AI inference server – a Flask app that loads a tiny TensorFlow model (MNIST digit recognizer) and serves predictions via HTTP.
- 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.
- 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
3️⃣ Step‑by‑Step Implementation
3.1 Initialise the repo
mkdir crypto-ai-marketplace && cd crypto-ai-marketplace
git init
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"
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;
}
}
3.2.2 Install OpenZeppelin contracts
npm i @openzeppelin/contracts
3.2.3 Compile & test
npx hardhat compile
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
-
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
-
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;
});
- Deploy
npx hardhat run scripts/deploy.ts --network sepolia
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
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)
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"]
requirements.txt:
flask
tensorflow==2.13.*
pillow
numpy
web3
gunicorn
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
Test with Postman:
GET http://localhost:8000/health → {"status":"ok"}
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
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)
})
});
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>
);
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
Top comments (0)