đ ď¸ EndâtoâEnd Tutorial
Build an AIâpowered cryptoâpriceâprediction service that logs every prediction onâchain
Youâll end up with:
| Component | Tech Stack | What it does |
|---|---|---|
| Data ingestion | PythonâŻ+âŻCoinGecko API | Pulls historic OHLCV data |
| Model | TensorFlow (LSTM) | Trains a shortâterm priceâforecast model |
| API | FastAPI | Serves GET /predict and POST /log endpoints |
| Smart contract | Solidity (Ethereum) | Stores each prediction (timestamp, price) in an immutable ledger |
| Frontâend | ReactâŻ+âŻVite | Shows the latest prediction & lets users submit their own |
| Deployment | DockerâŻCompose (local) â AWS ECS / GCP Cloud Run (optional) | Oneâclick spinâup of the whole stack |
1ď¸âŁ Prerequisites
| Category | Required |
|---|---|
| Programming | PythonâŻâĽâŻ3.10, JavaScript/TypeScript, basic Solidity |
| Tools | Git, DockerâŻâĽâŻ20.10, NodeâŻâĽâŻ18, npmâŻâĽâŻ9, VSâŻCode (or any IDE) |
| Accounts | Free Infura or Alchemy project (Ethereum RPC), Etherscan API key (optional) |
| Crypto | Testnet ETH (e.g., Sepolia) â get via a faucet |
| Knowledge | 1âline basics of REST, LSTM, smart contracts, and Docker |
Tip: If youâre new to any of these, skim the official âGetting Startedâ docs first â the tutorial works even if you only know the basics.
2ď¸âŁ Project Layout
cryptoâaiâpredictor/
ââ backend/ # FastAPI + ML model
â ââ app/
â â ââ main.py
â â ââ model.py
â â ââ utils.py
â ââ Dockerfile
â ââ requirements.txt
ââ contracts/ # Solidity contract + deployment scripts
â ââ PredictionLogger.sol
â ââ scripts/
â ââ deploy.ts
ââ frontend/ # React UI
â ââ src/
â â ââ App.tsx
â â ââ api.ts
â ââ Dockerfile
ââ docker-compose.yml
ââ README.md
Weâll fill each folder stepâbyâstep.
3ď¸âŁ StepâbyâStep Implementation
3.1 Set up the repository
git clone https://github.com/yourname/crypto-ai-predictor.git
cd crypto-ai-predictor
Create a Python virtual environment (optional â Docker will handle it later):
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
3.2 Backend â Data, Model, API
3.2.1 Install Python dependencies
Create backend/requirements.txt:
fastapi==0.110.0
uvicorn[standard]==0.29.0
pandas==2.2.2
numpy==1.26.4
scikit-learn==1.5.0
tensorflow==2.16.1
python-dotenv==1.0.1
requests==2.32.3
web3==6.19.0
pip install -r backend/requirements.txt
3.2.2 Pull historic price data
Create backend/app/utils.py:
import os
import requests
import pandas as pd
from datetime import datetime, timedelta
COINGECKO_API = "https://api.coingecko.com/api/v3"
DEFAULT_COIN = "bitcoin"
DEFAULT_VS_CURRENCY = "usd"
def fetch_ohlcv(days: int = 90, coin: str = DEFAULT_COIN) -> pd.DataFrame:
"""
Returns a DataFrame with columns:
['timestamp','open','high','low','close','volume']
"""
# CoinGecko returns daily candles for the last N days
url = f"{COINGECKO_API}/coins/{coin}/ohlc"
params = {"vs_currency": DEFAULT_VS_CURRENCY, "days": days}
resp = requests.get(url, params=params)
resp.raise_for_status()
data = resp.json() # [[unix, open, high, low, close], ...]
df = pd.DataFrame(data, columns=["timestamp", "open", "high", "low", "close"])
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
# Estimate volume (CoinGecko does not give it in the OHLC endpoint)
# We'll fetch market data and calculate a proxy
vol_url = f"{COINGECKO_API}/coins/{coin}/market_chart"
vol_params = {"vs_currency": DEFAULT_VS_CURRENCY, "days": days}
vol_resp = requests.get(vol_url, params=vol_params)
vol_resp.raise_for_status()
vol_data = vol_resp.json()["total_volumes"] # [[unix, volume], ...]
vol_df = pd.DataFrame(vol_data, columns=["timestamp", "volume"])
vol_df["timestamp"] = pd.to_datetime(vol_df["timestamp"], unit="ms")
df = df.merge(vol_df, on="timestamp")
df.set_index("timestamp", inplace=True)
return df
Explanation â CoinGeckoâs free tier gives us 90âday daily candles without any API key. The function merges volume data to give a full OHLCV dataset.
3.2.3 Build a simple LSTM model
Create backend/app/model.py:
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras import layers, models, callbacks
from sklearn.preprocessing import MinMaxScaler
from .utils import fetch_ohlcv
# ----------------------------------------------------------------------
# 1ď¸âŁ Data preprocessing
# ----------------------------------------------------------------------
def prepare_dataset(df: pd.DataFrame, lookback: int = 30) -> tuple:
"""
Returns (X, y) where:
X shape = (samples, lookback, features)
y shape = (samples, 1) -> next day's closing price
"""
scaler = MinMaxScaler()
scaled = scaler.fit_transform(df)
X, y = [], []
for i in range(len(scaled) - lookback):
X.append(scaled[i : i + lookback])
y.append(scaled[i + lookback, 3]) # column 3 = close
X = np.array(X)
y = np.array(y).reshape(-1, 1)
return X, y, scaler
# ----------------------------------------------------------------------
# 2ď¸âŁ Model definition
# ----------------------------------------------------------------------
def build_lstm(input_shape):
model = models.Sequential([
layers.LSTM(64, activation='tanh', input_shape=input_shape),
layers.Dense(32, activation='relu'),
layers.Dense(1) # predict scaled close price
])
model.compile(optimizer='adam', loss='mse')
return model
# ----------------------------------------------------------------------
# 3ď¸âŁ Training routine (called from main)
# ----------------------------------------------------------------------
def train_and_save(model_path: str = "model.h5", lookback: int = 30):
df = fetch_ohlcv(days=180) # 6âmonths of data for better generalisation
X, y, scaler = prepare_dataset(df, lookback)
model = build_lstm(input_shape=X.shape[1:])
es = callbacks.EarlyStopping(patience=10, restore_best_weights=True)
model.fit(X, y, epochs=200, batch_size=16, validation_split=0.2, callbacks=[es])
# Save both model and scaler (pickle)
model.save(model_path)
import joblib, pathlib
pathlib.Path("scaler.pkl").write_bytes(joblib.dumps(scaler))
print(f"â
Model saved to {model_path}")
Why LSTM? It captures temporal dependencies in price series with few parametersâperfect for a demo.
3.2.4 FastAPI server
Create backend/app/main.py:
import os
import json
import numpy as np
import pandas as pd
import joblib
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from tensorflow.keras.models import load_model
from .utils import fetch_ohlcv
from .model import prepare_dataset
app = FastAPI(title="Crypto AI Predictor", version="0.1.0")
# --------------------------------------------------------------
# Load model & scaler at startup
# --------------------------------------------------------------
MODEL_PATH = os.getenv("MODEL_PATH", "model.h5")
SCALER_PATH = os.getenv("SCALER_PATH", "scaler.pkl")
model = load_model(MODEL_PATH)
scaler = joblib.load(SCALER_PATH)
LOOKBACK = 30 # same as in training
class PredictResponse(BaseModel):
timestamp: str
predicted_price: float
confidence: float | None = None # placeholder for future extension
# --------------------------------------------------------------
# Helper: turn latest OHLCV into a prediction tensor
# --------------------------------------------------------------
def get_latest_tensor() -> np.ndarray:
df = fetch_ohlcv(days=LOOKBACK + 1) # we need LOOKBACK rows
# Keep same column order as during training
df = df[["open", "high", "low", "close", "volume"]]
scaled = scaler.transform(df)
tensor = scaled[-LOOKBACK:] # shape (lookback, 5)
return tensor.reshape((1, LOOKBACK, 5))
# --------------------------------------------------------------
# Public endpoint â return nextâday price prediction
# --------------------------------------------------------------
@app.get("/predict", response_model=PredictResponse)
def predict():
try:
tensor = get_latest_tensor()
pred_scaled = model.predict(tensor)[0][0] # scalar
# Inverseâscale only the *close* column (index 3)
dummy = np.zeros((1, scaler.n_features_in_))
dummy[0, 3] = pred_scaled
pred_price = scaler.inverse_transform(dummy)[0, 3]
ts = pd.Timestamp.utcnow().isoformat()
return PredictResponse(timestamp=ts, predicted_price=round(float(pred_price), 2))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# --------------------------------------------------------------
# Optional: endpoint to trigger a reâtrain (protected in prod)
# --------------------------------------------------------------
@app.post("/train")
def train():
from .model import train_and_save
train_and_save(model_path=MODEL_PATH)
# reload
global model, scaler
model = load_model(MODEL_PATH)
scaler = joblib.load(SCALER_PATH)
return {"status": "retrained"}
Explanation â
GET /predictgrabs the most recent 30 days, scales them, feeds to the LSTM, then deâscales the predicted close price.POST /trainis a convenience for local testing; in production youâd protect it with an API key or CI pipeline.
3.2.5 Dockerize the backend
Create backend/Dockerfile:
# syntax = docker/dockerfile:1.4
FROM python:3.12-slim AS builder
WORKDIR /app
COPY backend/requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# ---- Runtime image ----
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY backend/app ./app
# Model artefacts (you can also mount them as volumes)
COPY backend/model.h5 .
COPY backend/scaler.pkl .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Why multiâstage? Keeps the final image tiny (~70âŻMB) â perfect for serverless containers.
3.3 Smart Contract â Log Predictions OnâChain
3.3.1 Solidity contract
Create contracts/PredictionLogger.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract PredictionLogger {
struct Prediction {
uint256 timestamp; // block timestamp when logged
uint256 price; // price * 1e2 (e.g., $27,345.12 â 2734512)
address reporter; // who logged it
}
Prediction[] public predictions;
event PredictionLogged(uint256 indexed idx, uint256 timestamp, uint256 price, address reporter);
/// @notice Store a new prediction. Caller pays only gas.
/// @param price price in cents (2 decimals) to avoid floating points.
function logPrediction(uint256 price) external {
predictions.push(Prediction({
timestamp: block.timestamp,
price: price,
reporter: msg.sender
}));
emit PredictionLogged(predictions.length - 1, block.timestamp, price, msg.sender);
}
/// @notice Get total number of predictions logged.
function count() external view returns (uint256) {
return predictions.length;
}
/// @notice Retrieve a prediction by index.
function get(uint256 idx) external view returns (Prediction memory) {
require(idx < predictions.length, "out of range");
return predictions[idx];
}
}
Design notes
- We store price as an integer with 2 decimal places (
uint256 price). Solidity has no floatingâpoint numbers.logPredictionis payableâfree â anyone can call it, but you could add anonlyOwneror a small fee later.
3.3.2 Hardhat setup (deployment script)
cd contracts
npm init -y
npm i --save-dev hardhat @nomicfoundation/hardhat-toolbox ethers dotenv
npx hardhat
# Choose "Create a basic sample project"
Add .env in contracts/:
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
PRIVATE_KEY=0xYOUR_PRIVATE_KEY # account with testnet ETH
Update hardhat.config.ts:
import { config as dotenvConfig } from "dotenv";
import { HardhatUserConfig } from "hardhat/types";
dotenvConfig();
const config: HardhatUserConfig = {
solidity: "0.8.24",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};
export default config;
Create scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const PredictionLogger = await ethers.getContractFactory("PredictionLogger");
const logger = await PredictionLogger.deploy();
await logger.waitForDeployment();
console.log("â
PredictionLogger deployed to:", await logger.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Deploy to Sepolia:
npx hardhat run scripts/deploy.ts --network sepolia
# copy the printed address â youâll need it in the backend
3.3.3 ABI export
After compilation, the ABI is in artifacts/contracts/PredictionLogger.sol/PredictionLogger.json. Copy the abi array into the backend folder:
mkdir -p backend/app/abi
cp artifacts/contracts/PredictionLogger.sol/PredictionLogger.json backend/app/abi/
Rename it to PredictionLogger_abi.json for clarity.
3.4 Connect FastAPI to the Contract
Add web3 to the backend (already in requirements.txt).
Create backend/app/blockchain.py:
python
import os
from web3 import Web3
import json
from pathlib import Path
# Load env variables (you can use python-dotenv)
INFURA_URL = os.getenv("INFURA_URL") # e.g. https://sepolia.infura.io/v3/xxxx
PRIVATE_KEY = os.getenv("PRIVATE_KEY") # for signing txs (test account)
w3 = Web3(Web3.HTTPProvider(INFURA_URL))
assert w3.is_connected(), "â Can't connect to Ethereum node"
# Load contract ABI & address
ABI_PATH = Path(__file__).parent / "abi" / "PredictionLogger_abi.json"
with open(ABI_PATH) as f:
abi = json.load(f)["abi"]
CONTRACT_ADDRESS = os.getenv("CONTRACT_ADDRESS") # set after deployment
contract = w3.eth.contract(address=CONTRACT_ADDRESS, abi=abi)
def log_prediction_onchain(price_usd: float) -> str:
"""
Sends a transaction to store `price_usd` (2 decimal precision) onâchain.
Returns the transaction hash.
"""
# Convert to integer cents
price_cents = int(round(price_usd * 100))
# Build transaction
nonce = w3.eth.get_transaction_count(w3.eth.account.from_key(PRIVATE_KEY).address)
tx = contract.functions.logPrediction(price_cents).build_transaction({
"chainId": w3.eth.chain_id,
"gas": 200_000,
"gasPrice": w3.to_wei("5", "gwei"),
#coding #tutorial #web3 #AI
Top comments (0)