Fix coordinator CORS function name and add marketplace matching endpoints with miner poll improvements
Some checks failed
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled

This commit is contained in:
aitbc
2026-05-27 11:58:33 +02:00
parent 1f4a630964
commit 0314e2da7c
7 changed files with 448 additions and 22 deletions

View File

@@ -21,7 +21,7 @@ def create_app() -> FastAPI:
app.add_middleware(
CORSMiddleware,
allow_origins=_validated_cors_origins(settings.cors_origins),
allow_origins=validated_cors_origins(settings.cors_origins),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

View File

@@ -24,6 +24,7 @@ from aitbc import (
from .storage import init_db, get_session
from .services.marketplace_service import MarketplaceService
from .services.matching_service import MatchingService
# Configure structured logging
configure_logging(level="INFO")
@@ -122,6 +123,11 @@ async def get_marketplace_service(session: AsyncSession = Depends(get_session))
return MarketplaceService(session)
async def get_matching_service(session: AsyncSession = Depends(get_session)) -> MatchingService:
"""Get matching service instance"""
return MatchingService(session)
@app.get("/v1/marketplace/offers")
async def get_offers(
status: str | None = None,
@@ -256,6 +262,82 @@ async def get_analytics(
return await svc.get_analytics(period_type=period_type)
@app.post("/v1/marketplace/match")
async def find_match(
bid_requirements: dict,
max_price: float | None = None,
preferred_region: str | None = None,
min_gpu_memory: int | None = None,
required_gpu_model: str | None = None,
matching_svc: MatchingService = Depends(get_matching_service),
):
"""Find best matching offer for bid requirements"""
try:
logger.info(f"POST /v1/marketplace/match called with requirements: {bid_requirements.keys()}")
result = await matching_svc.find_best_match(
bid_requirements=bid_requirements,
max_price=max_price,
preferred_region=preferred_region,
min_gpu_memory=min_gpu_memory,
required_gpu_model=required_gpu_model
)
logger.info(f"Match result: {result is not None}")
return result or {"message": "No matching offer found"}
except Exception as e:
logger.error(f"Error in POST /v1/marketplace/match: {type(e).__name__}: {str(e)}")
raise
@app.post("/v1/marketplace/matches")
async def create_match(
bid_id: str,
offer_id: str,
match_data: dict,
matching_svc: MatchingService = Depends(get_matching_service),
):
"""Create a match between a bid and an offer"""
try:
logger.info(f"POST /v1/marketplace/matches called: bid_id={bid_id}, offer_id={offer_id}")
result = await matching_svc.create_match(bid_id, offer_id, match_data)
logger.info(f"Created match: {result['match_id']}")
return result
except Exception as e:
logger.error(f"Error in POST /v1/marketplace/matches: {type(e).__name__}: {str(e)}")
raise
@app.get("/v1/marketplace/matches")
async def list_matches(
status: str | None = None,
provider: str | None = None,
matching_svc: MatchingService = Depends(get_matching_service),
):
"""List all matches"""
try:
logger.info(f"GET /v1/marketplace/matches called with filters: status={status}, provider={provider}")
result = await matching_svc.list_matches(status=status, provider=provider)
logger.info(f"Found {len(result)} matches")
return {"matches": result}
except Exception as e:
logger.error(f"Error in GET /v1/marketplace/matches: {type(e).__name__}: {str(e)}")
raise
@app.post("/v1/marketplace/matches/auto")
async def auto_match(
matching_svc: MatchingService = Depends(get_matching_service),
):
"""Automatically match all pending bids with available offers"""
try:
logger.info("POST /v1/marketplace/matches/auto called")
result = await matching_svc.auto_match_pending_bids()
logger.info(f"Auto-match complete: {result}")
return result
except Exception as e:
logger.error(f"Error in POST /v1/marketplace/matches/auto: {type(e).__name__}: {str(e)}")
raise
@app.get("/v1/marketplace/plugins")
async def get_plugins(
type: str | None = None,

View File

@@ -3,5 +3,6 @@ Marketplace Service services
"""
from .marketplace_service import MarketplaceService
from .matching_service import MatchingService
__all__ = ["MarketplaceService"]
__all__ = ["MarketplaceService", "MatchingService"]

View File

@@ -0,0 +1,299 @@
"""
Marketplace matching service for matching GPU providers with consumers
"""
from typing import Any, Optional
from datetime import datetime, timezone
from sqlmodel import select
from sqlalchemy.ext.asyncio import AsyncSession
from aitbc import get_logger
from ..domain.marketplace import MarketplaceOffer, MarketplaceBid
logger = get_logger(__name__)
class MatchingService:
"""Service for matching GPU offers with consumer bids"""
def __init__(self, session: AsyncSession):
self.session = session
async def find_best_match(
self,
bid_requirements: dict,
max_price: Optional[float] = None,
preferred_region: Optional[str] = None,
min_gpu_memory: Optional[int] = None,
required_gpu_model: Optional[str] = None
) -> Optional[dict]:
"""
Find the best matching offer for a bid based on requirements
Args:
bid_requirements: Bid requirements (capacity, duration, etc.)
max_price: Maximum price per hour willing to pay
preferred_region: Preferred region for GPU location
min_gpu_memory: Minimum GPU memory in GB
required_gpu_model: Specific GPU model required
Returns:
Best matching offer as dict, or None if no match found
"""
try:
logger.info(f"Finding best match for bid requirements: {bid_requirements.keys()}")
# Build query for available offers
stmt = select(MarketplaceOffer).where(MarketplaceOffer.status == "active")
# Apply filters
if max_price:
stmt = stmt.where(MarketplaceOffer.price_per_hour <= max_price)
if preferred_region:
stmt = stmt.where(MarketplaceOffer.region == preferred_region)
if min_gpu_memory:
stmt = stmt.where(MarketplaceOffer.gpu_memory_gb >= min_gpu_memory)
if required_gpu_model:
stmt = stmt.where(MarketplaceOffer.gpu_model == required_gpu_model)
# Order by price (lowest first) and capacity (highest first)
stmt = stmt.order_by(
MarketplaceOffer.price_per_hour.asc(),
MarketplaceOffer.capacity.desc()
)
result = await self.session.execute(stmt)
offers = result.scalars().all()
if not offers:
logger.info("No matching offers found")
return None
# Return best match (first result due to ordering)
best_offer = offers[0]
logger.info(f"Found best match: offer_id={best_offer.id}, price={best_offer.price_per_hour}")
return {
'id': best_offer.id,
'provider': best_offer.provider,
'capacity': best_offer.capacity,
'price': best_offer.price,
'price_per_hour': best_offer.price_per_hour,
'gpu_model': best_offer.gpu_model,
'gpu_memory_gb': best_offer.gpu_memory_gb,
'gpu_count': best_offer.gpu_count,
'region': best_offer.region,
'match_score': self._calculate_match_score(best_offer, bid_requirements),
}
except Exception as e:
logger.error(f"Error in find_best_match: {type(e).__name__}: {str(e)}")
raise
def _calculate_match_score(self, offer: MarketplaceOffer, requirements: dict) -> float:
"""
Calculate a match score for an offer based on requirements
Args:
offer: Marketplace offer
requirements: Bid requirements
Returns:
Match score between 0.0 and 1.0
"""
score = 1.0
# Penalize higher prices
if requirements.get('max_price'):
price_ratio = offer.price_per_hour / requirements['max_price']
score *= (1.0 - price_ratio * 0.3) # Price contributes 30% to score
# Bonus for higher capacity
if requirements.get('capacity'):
capacity_ratio = min(offer.capacity / requirements['capacity'], 2.0)
score *= (0.7 + capacity_ratio * 0.15) # Capacity contributes 15%
# Bonus for region match
if requirements.get('preferred_region') and offer.region == requirements['preferred_region']:
score *= 1.1 # 10% bonus for region match
return min(max(score, 0.0), 1.0)
async def create_match(
self,
bid_id: str,
offer_id: str,
match_data: dict
) -> dict:
"""
Create a match between a bid and an offer
Args:
bid_id: ID of the bid
offer_id: ID of the offer
match_data: Additional match data
Returns:
Match record as dict
"""
try:
logger.info(f"Creating match: bid_id={bid_id}, offer_id={offer_id}")
# Get bid and offer
bid_stmt = select(MarketplaceBid).where(MarketplaceBid.id == bid_id)
bid_result = await self.session.execute(bid_stmt)
bid = bid_result.scalar_one_or_none()
if not bid:
raise ValueError(f"Bid not found: {bid_id}")
offer_stmt = select(MarketplaceOffer).where(MarketplaceOffer.id == offer_id)
offer_result = await self.session.execute(offer_stmt)
offer = offer_result.scalar_one_or_none()
if not offer:
raise ValueError(f"Offer not found: {offer_id}")
# Update bid status to matched
bid.status = "matched"
bid.notes = f"Matched with offer {offer_id}"
# Update offer status if capacity is fully utilized
if offer.capacity <= bid.capacity:
offer.status = "booked"
else:
offer.capacity -= bid.capacity
await self.session.commit()
match_record = {
'match_id': f"match_{bid_id[:8]}_{offer_id[:8]}",
'bid_id': bid_id,
'offer_id': offer_id,
'provider': offer.provider,
'consumer': bid.provider,
'price_per_hour': offer.price_per_hour,
'capacity': bid.capacity,
'status': 'active',
'created_at': datetime.now(timezone.utc).isoformat(),
'match_score': match_data.get('match_score', 0.0),
}
logger.info(f"Created match: {match_record['match_id']}")
return match_record
except Exception as e:
await self.session.rollback()
logger.error(f"Error in create_match: {type(e).__name__}: {str(e)}")
raise
async def list_matches(
self,
status: Optional[str] = None,
provider: Optional[str] = None
) -> list[dict]:
"""
List all matches (derived from matched bids)
Args:
status: Filter by match status
provider: Filter by provider
Returns:
List of match records
"""
try:
logger.info(f"Listing matches with filters: status={status}, provider={provider}")
# Query matched bids
stmt = select(MarketplaceBid).where(MarketplaceBid.status == "matched")
if provider:
stmt = stmt.where(MarketplaceBid.provider == provider)
result = await self.session.execute(stmt)
bids = result.scalars().all()
matches = []
for bid in bids:
# Extract offer_id from notes
offer_id = None
if bid.notes and "Matched with offer" in bid.notes:
offer_id = bid.notes.split()[-1]
matches.append({
'bid_id': bid.id,
'offer_id': offer_id,
'provider': bid.provider,
'capacity': bid.capacity,
'price': bid.price,
'status': bid.status,
'created_at': bid.submitted_at.isoformat() if bid.submitted_at else None,
})
logger.info(f"Found {len(matches)} matches")
return matches
except Exception as e:
logger.error(f"Error in list_matches: {type(e).__name__}: {str(e)}")
raise
async def auto_match_pending_bids(self) -> dict:
"""
Automatically match all pending bids with available offers
Returns:
Summary of matching results
"""
try:
logger.info("Starting auto-match for pending bids")
# Get all pending bids
stmt = select(MarketplaceBid).where(MarketplaceBid.status == "pending")
result = await self.session.execute(stmt)
pending_bids = result.scalars().all()
matched_count = 0
failed_count = 0
for bid in pending_bids:
try:
# Find best match
match = await self.find_best_match(
bid_requirements={
'capacity': bid.capacity,
'max_price': bid.price,
}
)
if match:
# Create the match
await self.create_match(
bid_id=bid.id,
offer_id=match['id'],
match_data=match
)
matched_count += 1
else:
failed_count += 1
except Exception as e:
logger.error(f"Failed to match bid {bid.id}: {e}")
failed_count += 1
summary = {
'total_pending': len(pending_bids),
'matched': matched_count,
'failed': failed_count,
'timestamp': datetime.now(timezone.utc).isoformat(),
}
logger.info(f"Auto-match complete: {summary}")
return summary
except Exception as e:
logger.error(f"Error in auto_match_pending_bids: {type(e).__name__}: {str(e)}")
raise

View File

@@ -8,6 +8,7 @@ import time
import sys
import subprocess
import os
import logging
from datetime import datetime, timezone
from typing import Dict, Optional
@@ -188,12 +189,13 @@ def register_miner():
headers = {
"X-Api-Key": AUTH_TOKEN,
"X-Miner-ID": MINER_ID,
"Content-Type": "application/json"
}
try:
client = AITBCHTTPClient(base_url=COORDINATOR_URL, headers=headers, timeout=10)
response = client.post(f"/v1/miners/register?miner_id={MINER_ID}", json=register_data)
response = client.post("/v1/miners/register", json=register_data)
if response:
logger.info(f"Successfully registered miner: {response}")
@@ -239,12 +241,13 @@ def send_heartbeat():
headers = {
"X-Api-Key": AUTH_TOKEN,
"X-Miner-ID": MINER_ID,
"Content-Type": "application/json"
}
try:
client = AITBCHTTPClient(base_url=COORDINATOR_URL, headers=headers, timeout=5)
response = client.post(f"/v1/miners/heartbeat?miner_id={MINER_ID}", json=heartbeat_data)
response = client.post("/v1/miners/heartbeat", json=heartbeat_data)
if response:
logger.info(f"Heartbeat sent (GPU: {gpu_info['utilization'] if gpu_info else 'N/A'}%)")
@@ -374,21 +377,36 @@ def poll_for_jobs():
headers = {
"X-Api-Key": AUTH_TOKEN,
"X-Miner-ID": MINER_ID,
"Content-Type": "application/json"
}
try:
client = AITBCHTTPClient(base_url=COORDINATOR_URL, headers=headers, timeout=10)
response = client.post("/v1/miners/poll", json=poll_data)
if response:
job = response
# Use requests directly to handle 204 No Content properly
import requests
url = f"{COORDINATOR_URL}/v1/miners/poll"
response = requests.post(url, json=poll_data, headers=headers, timeout=10)
if response.status_code == 204:
# No jobs available
return None
response.raise_for_status()
job = response.json()
if job and job.get("job_id"):
logger.info(f"Received job: {job}")
return job
else:
return None
except NetworkError as e:
except requests.exceptions.HTTPError as e:
if e.response.status_code == 204:
logger.debug("No jobs available (204 No Content)")
return None
logger.error(f"HTTP error polling for jobs: {e}")
return None
except Exception as e:
logger.error(f"Error polling for jobs: {e}")
return None
@@ -399,18 +417,23 @@ def main():
# Check GPU availability
gpu_info = get_gpu_info()
if not gpu_info:
logger.error("GPU not available, exiting")
# sys.exit(1)
logger.info(f"GPU detected: {gpu_info['name']} ({gpu_info['memory_total']}MB)")
logger.warning("GPU not available, running in CPU-only mode")
gpu_info = {
"name": "CPU-Only",
"memory_total": 0,
"memory_used": 0,
"utilization": 0
}
else:
logger.info(f"GPU detected: {gpu_info['name']} ({gpu_info['memory_total']}MB)")
# Check Ollama
ollama_available, models = check_ollama()
if not ollama_available:
logger.error("Ollama not available - please install and start Ollama")
# sys.exit(1)
logger.info(f"Ollama models available: {', '.join(models)}")
logger.warning("Ollama not available - miner will not be able to execute inference jobs")
models = []
else:
logger.info(f"Ollama models available: {', '.join(models)}")
# Wait for coordinator
if not wait_for_coordinator():

View File

@@ -1,8 +1,8 @@
{
"0x1234567890123456789012345678901234567890": {
"private_key_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc0VfRmLKclWkN\nYO5NCf7MT4ss8JMkRKMmpwUhEN/BnDyCGxgAo+po88KCHoHLm5JiWIDLTALnQkgQ\nXXL8DpCaXwMtBICzbq/zfDye7L114+lcm3CXSRhmELmLV+zo29PiF5UNV52m4ZfV\n3O1ophFDQ0XnGFoo1eG1eflzdMYCOSoxjrV/Z0ltloc3P+O1wSXcttNw4DQ9fqnp\nDRTjZWKyVSz2dI6vlnObJszfntykzWmJ2YU1TtFdh810SU3LT1fV72icbWm1KjF5\nO7T2++Wl+JJ13D8cIBiwVruWuCMno8D5YfKC35uRS8Ob4wmMDQEnQDACEKXmR4B5\neQ4SEtZ9AgMBAAECggEABj2OcRjSgsivVYj18rrjGN5Re4hXUqook/Exkw9I2DuP\nbN4HJn9fZK3On773C1M1kBRVi8GKnAlXNM+DM+SgfIQrbC8xr/JHrjjTcL+bCoX3\nU2gcIukVv3oK6DCnjNyyodyuYcmKzIlNsYUJLZDuPu7+aSPe8qEQSliARMfw2UW9\nJWQMLfiyvC4NDh6Wem4Cl+LmiKMtx1DNYe9rgSSt30XyeGopNGaxiMStOifKIo4g\nweqjIQXxyIgmkCGrIPC4NUVIltHFzp9YrNpxdsbR3Il4ycrLPVuwErF4y+/iJfj4\ncNSxGPkHZzfAOmfYEgGKfU+l7Z6pPsf3oiqTNgsWAQKBgQDK4/vXeUU6rozPYpSY\nSsymL9uE8Kw6Ves3sIr8XO3KisXaWrov33RTgrDV8hwc3Z3yDFz1vgHUVlo2sJoY\nl/8mjD+RHsAyQNIU8IQsxUSLo3HftmEUa0xz9LQuHgF1CXFsOIBlXGeaVFsyp40Q\nYzL9wjr9ooXreF0R5Np4I69taQKBgQDF3fKETeZOqPkbJsP43SowkZ41z1kKJomP\nyuImmt3G4hSmAiUCSqV4GWvz5rxwPCpuD//91KSp8DcZ4LeGnPKhx5PIKv2lWrWg\n1V4s+4Ea27hu5NKfrpOAxOgZgUjt748mPs6D9Mmi+w8/EvRbIzJO5M964wSOtHi4\n+OsDzyT59QKBgFPotd8HaHo8dj/OpWXWiYyxfjgc0R3PKth9Sv3T8QQzIGCN5TKn\nV5SyGDBjUP0fKpNQSaHYUyleDTFRGGnTctKebiu2bAZciIXgcsmRTCf0EMRUyRGI\nzrWmHl50SmX84cvAElnZPX+2I4Fvigec/xmzmnILJRedT+B2pWPKXmMBAoGAU7+k\ndWFveJ3Gijp3Oi+KOvJ3j3kKy+QR133dCNAFzLdGXBmORpEHxnSkH6Dq42pj3yAA\njxRg+djFybs2ktB9VgJeR5wCreld9Qw6hzmQpKiZQL6zc4j1v8wYHSt+jc8WvO5a\nhLmoWsZ+5oiESsrz8TahpvbNqAU1D72z43Hayb0CgYA8N+FkGN+FAAQ4Pahvgmb8\njeqHUUPRbbq2tvCknB0VBuU5WOstRFjJWADMDFevHvmx/32yhadQP8C2eS0OQOkW\n6uqIPrLfwfQNyQWtNi+q5+par23ITDjv1RlptgDcOT1IVoo4D9EpaLhzBFHYONYU\nakhZt0S6Rf0IVxot+q+oOg==\n-----END PRIVATE KEY-----\n",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNFX0ZiynJVpDWDuTQn+\nzE+LLPCTJESjJqcFIRDfwZw8ghsYAKPqaPPCgh6By5uSYliAy0wC50JIEF1y/A6Q\nml8DLQSAs26v83w8nuy9dePpXJtwl0kYZhC5i1fs6NvT4heVDVedpuGX1dztaKYR\nQ0NF5xhaKNXhtXn5c3TGAjkqMY61f2dJbZaHNz/jtcEl3LbTcOA0PX6p6Q0U42Vi\nslUs9nSOr5ZzmybM357cpM1pidmFNU7RXYfNdElNy09X1e9onG1ptSoxeTu09vvl\npfiSddw/HCAYsFa7lrgjJ6PA+WHygt+bkUvDm+MJjA0BJ0AwAhCl5keAeXkOEhLW\nfQIDAQAB\n-----END PUBLIC KEY-----\n",
"created_at": 1776106994.919443,
"last_rotated": 1776106994.9196188
"private_key_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHN6OklsaBWneN\njGkYKG9/r/vRu4abdX3ahykmPd5I6QnXxobl85QUPKsMqPu6Rx+5l9KPW85kpXBm\n/QwZOxgukFqd3KZEoZ3yy0IHSIpv6vmr9mBBkLKyT2sdYS1gWuJ3F6v7leLmoaU5\nIDsFcMDn+SctOZI32Zf9xF4VkMJDEhx05xdIbumvIpSoj4MXyXQZLfsHyoOWrH5k\nOKYL/t/6gxeBfxy+Yw9AvkldLrqTnbx9x6ewvV+f9VciKT0h56BX/197KLCMlmZo\nTB5QpdUpFEnFwAQcC81Yzs0t77LrWoFgpAoYPU0lWHvr250+h6HGgFCPkjcNtjkJ\ndtvtnk5PAgMBAAECggEASVyd8JBtjVkJSaD5WqIZXUYrT7LlAP6lWAIKD0EdSHA4\n5bMAHlIyp7knrEPWX2SttCTKr2w5dyrNV7+74ta2Mv+JvzRwLjnt9mkPaas2/7vi\nMYdLLxngFHXWlj0g/qi5WO3osX8izZedRoot8fTxtPs1iBv5UoPYyuSzWPGz+Ape\nonX9I9/ILdPFiW2ruq1PB8anmplIZZ2YbaijLXnZBpx30HEssntrHGCfq3Q1hgU1\nd+LEtL9KA7T9H7hRmp82E3DZGJIzjuHVJO4Wcdb5f4U3xj28QISA7maqn5dA9NT+\nDRz+33tjD52o/V0ymIKo3u3WtNMLhwAl1AhY1Z6yQQKBgQDyBldIhulDNAExDrdg\nH9yZd6ZnavpAbdr+9Kdpra43Hwa1QCpqdsYbpiIg5vBxBCn749AIhWEHpAzO7puP\ns61sa7IiISU6OVlRCpzqqSrcY+GpFCIyMXoHZwQR90g7jghbFMRm1g53sBjmZYli\nl1lDOzKYiz2SNZa/MfCuUmXXHwKBgQDSuIKdwEK3f9yjbH7J106SqJj1SzYwdaij\nMNhGjORfnmRy66IpPDgvaS6gBzS1hT1ympnoykgN3q9OgrZKOETuHoHuhfT+Bnfp\nfEjN3bDHnDP4wzV1cBvuLfYrI/cUdbfzUN6Dc81CxIVPnFyKO7F2b7zo7nCEPIlU\nVQIgpPaS0QKBgQCT+fiH6aTZaASKgBrydMimNJfTh372wbQySlfJr11jal7plw/Y\nBELgSNV5FHpSP1+EGSfq7dIDn/QM2arXU95m+fnyEB342XOYr0p912zTT2Z7wEmg\nMswPlpbQfUb20sKdHbdvwNUbrNmslMxJMYxsJNesmQXOTWGcCObFTq/htQKBgQDR\nJtR2cbOG4UGFcBX0j2Fszi1sIyf5N3+X4s54UDYI9nUrX9iH5z65SERAEIbvuP1B\nuFQVrFmScro8Sh9XUbyRQPSkZI/EZ3Uz6el1dJqXteIcAt4X35vJcBNLxJnk0+cu\nedEyVomgwOC1ITT0+8TsEoJGDQzfJBsG+o1vC222UQKBgQCJwmf8+RIqKFP3Lwcz\nQ96diGn+QqvWakrlaWZ7sPZamjytpLBFPmApUX1Q5+m7XqwXzWncqZReOJL+3VUV\nOCKJw5z79e8wWDjAtgLbFzxBMX/o6BoECUa7Sd0l8XAzBawPRog0s+qyXjiY2Ksk\nMKR4nzxr6Gns6ID35/tIBSZojw==\n-----END PRIVATE KEY-----\n",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzejpJbGgVp3jYxpGChv\nf6/70buGm3V92ocpJj3eSOkJ18aG5fOUFDyrDKj7ukcfuZfSj1vOZKVwZv0MGTsY\nLpBandymRKGd8stCB0iKb+r5q/ZgQZCysk9rHWEtYFridxer+5Xi5qGlOSA7BXDA\n5/knLTmSN9mX/cReFZDCQxIcdOcXSG7pryKUqI+DF8l0GS37B8qDlqx+ZDimC/7f\n+oMXgX8cvmMPQL5JXS66k528fcensL1fn/VXIik9IeegV/9feyiwjJZmaEweUKXV\nKRRJxcAEHAvNWM7NLe+y61qBYKQKGD1NJVh769udPoehxoBQj5I3DbY5CXbb7Z5O\nTwIDAQAB\n-----END PUBLIC KEY-----\n",
"created_at": 1779874874.5963404,
"last_rotated": 1779874874.596439
}
}

View File

@@ -0,0 +1,21 @@
[Unit]
Description=AITBC Production GPU Miner
After=network.target ollama.service aitbc-coordinator-api.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/aitbc/apps/miner
Environment="PATH=/opt/aitbc/venv/bin"
Environment="PYTHONPATH=/opt/aitbc"
Environment="COORDINATOR_URL=http://localhost:8011"
Environment="MINER_ID=aitbc-miner-1"
Environment="AUTH_TOKEN=aitbc-miner-token-secure"
ExecStart=/opt/aitbc/venv/bin/python /opt/aitbc/apps/miner/production_miner.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target