Files
aitbc/docs/developer/tutorials/building-custom-miner.md
oib 329b3beeba ```
feat: add SQLModel relationships, fix ZK verifier circuit integration, and complete Stage 19-20 documentation

- Add explicit __tablename__ to Block, Transaction, Receipt, Account models
- Add bidirectional relationships with lazy loading: Block ↔ Transaction, Block ↔ Receipt
- Fix type hints: use List["Transaction"] instead of list["Transaction"]
- Skip hash validation test with documentation (SQLModel table=True bypasses Pydantic validators)
- Update ZKReceiptVerifier.sol to match receipt_simple circuit (
2026-01-24 18:34:37 +01:00

8.0 KiB

Building a Custom Miner

This tutorial walks you through creating a custom GPU miner for the AITBC network.

Prerequisites

  • Linux system with NVIDIA GPU
  • Python 3.10+
  • CUDA toolkit installed
  • Ollama or other inference backend

Architecture Overview

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Coordinator    │────▶│   Your Miner     │────▶│  GPU Backend    │
│  API            │◀────│   (Python)       │◀────│  (Ollama)       │
└─────────────────┘     └──────────────────┘     └─────────────────┘

Your miner:

  1. Polls the Coordinator for available jobs
  2. Claims and processes jobs using your GPU
  3. Returns results and receives payment

Step 1: Basic Miner Structure

Create my_miner.py:

#!/usr/bin/env python3
"""Custom AITBC GPU Miner"""

import asyncio
import httpx
import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class CustomMiner:
    def __init__(self, coordinator_url: str, miner_id: str):
        self.coordinator_url = coordinator_url
        self.miner_id = miner_id
        self.client = httpx.AsyncClient(timeout=30.0)
        
    async def register(self):
        """Register miner with coordinator."""
        response = await self.client.post(
            f"{self.coordinator_url}/v1/miners/register",
            json={
                "miner_id": self.miner_id,
                "capabilities": ["llama3.2", "codellama"],
                "gpu_info": self.get_gpu_info()
            }
        )
        response.raise_for_status()
        logger.info(f"Registered as {self.miner_id}")
        
    def get_gpu_info(self) -> dict:
        """Collect GPU information."""
        try:
            import subprocess
            result = subprocess.run(
                ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
                capture_output=True, text=True
            )
            name, memory = result.stdout.strip().split(", ")
            return {"name": name, "memory": memory}
        except Exception:
            return {"name": "Unknown", "memory": "Unknown"}
    
    async def poll_jobs(self):
        """Poll for available jobs."""
        response = await self.client.get(
            f"{self.coordinator_url}/v1/jobs/available",
            params={"miner_id": self.miner_id}
        )
        if response.status_code == 200:
            return response.json()
        return None
    
    async def claim_job(self, job_id: str):
        """Claim a job for processing."""
        response = await self.client.post(
            f"{self.coordinator_url}/v1/jobs/{job_id}/claim",
            json={"miner_id": self.miner_id}
        )
        return response.status_code == 200
    
    async def process_job(self, job: dict) -> str:
        """Process job using GPU backend."""
        # Override this method with your inference logic
        raise NotImplementedError("Implement process_job()")
    
    async def submit_result(self, job_id: str, result: str):
        """Submit job result to coordinator."""
        response = await self.client.post(
            f"{self.coordinator_url}/v1/jobs/{job_id}/complete",
            json={
                "miner_id": self.miner_id,
                "result": result,
                "completed_at": datetime.utcnow().isoformat()
            }
        )
        response.raise_for_status()
        logger.info(f"Completed job {job_id}")
    
    async def run(self):
        """Main mining loop."""
        await self.register()
        
        while True:
            try:
                job = await self.poll_jobs()
                if job:
                    job_id = job["job_id"]
                    if await self.claim_job(job_id):
                        logger.info(f"Processing job {job_id}")
                        result = await self.process_job(job)
                        await self.submit_result(job_id, result)
                else:
                    await asyncio.sleep(2)  # No jobs, wait
            except Exception as e:
                logger.error(f"Error: {e}")
                await asyncio.sleep(5)

Step 2: Add Ollama Backend

Extend the miner with Ollama inference:

class OllamaMiner(CustomMiner):
    def __init__(self, coordinator_url: str, miner_id: str, ollama_url: str = "http://localhost:11434"):
        super().__init__(coordinator_url, miner_id)
        self.ollama_url = ollama_url
    
    async def process_job(self, job: dict) -> str:
        """Process job using Ollama."""
        prompt = job.get("prompt", "")
        model = job.get("model", "llama3.2")
        
        response = await self.client.post(
            f"{self.ollama_url}/api/generate",
            json={
                "model": model,
                "prompt": prompt,
                "stream": False
            },
            timeout=120.0
        )
        response.raise_for_status()
        return response.json()["response"]

# Run the miner
if __name__ == "__main__":
    miner = OllamaMiner(
        coordinator_url="https://aitbc.bubuit.net/api",
        miner_id="my-custom-miner-001"
    )
    asyncio.run(miner.run())

Step 3: Add Receipt Signing

Sign receipts for payment verification:

from aitbc_crypto import sign_receipt, generate_keypair

class SigningMiner(OllamaMiner):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.private_key, self.public_key = generate_keypair()
    
    async def submit_result(self, job_id: str, result: str):
        """Submit signed result."""
        receipt = {
            "job_id": job_id,
            "miner_id": self.miner_id,
            "result_hash": hashlib.sha256(result.encode()).hexdigest(),
            "completed_at": datetime.utcnow().isoformat()
        }
        
        signature = sign_receipt(receipt, self.private_key)
        receipt["signature"] = signature
        
        response = await self.client.post(
            f"{self.coordinator_url}/v1/jobs/{job_id}/complete",
            json={"result": result, "receipt": receipt}
        )
        response.raise_for_status()

Step 4: Run as Systemd Service

Create /etc/systemd/system/my-miner.service:

[Unit]
Description=Custom AITBC Miner
After=network.target ollama.service

[Service]
Type=simple
User=miner
WorkingDirectory=/home/miner
ExecStart=/usr/bin/python3 /home/miner/my_miner.py
Restart=always
RestartSec=10
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable my-miner
sudo systemctl start my-miner
sudo journalctl -u my-miner -f

Step 5: Monitor Performance

Add metrics collection:

import time

class MetricsMiner(SigningMiner):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.jobs_completed = 0
        self.total_time = 0
    
    async def process_job(self, job: dict) -> str:
        start = time.time()
        result = await super().process_job(job)
        elapsed = time.time() - start
        
        self.jobs_completed += 1
        self.total_time += elapsed
        
        logger.info(f"Job completed in {elapsed:.2f}s (avg: {self.total_time/self.jobs_completed:.2f}s)")
        return result

Best Practices

  1. Error Handling: Always catch and log exceptions
  2. Graceful Shutdown: Handle SIGTERM for clean exits
  3. Rate Limiting: Don't poll too aggressively
  4. GPU Memory: Monitor and clear GPU memory between jobs
  5. Logging: Use structured logging for debugging

Next Steps