```
chore: refactor logging module, update genesis timestamp, remove model relationships, and reorganize routers - Rename logging.py to logger.py and update import paths in poa.py and main.py - Update devnet genesis timestamp to 1766828620 - Remove SQLModel Relationship declarations from Block, Transaction, and Receipt models - Add SessionDep type alias and get_session dependency in coordinator-api deps - Reorganize coordinator-api routers: replace explorer/registry with exchange, users, marketplace
This commit is contained in:
631
docs/tutorials/marketplace-extensions.md
Normal file
631
docs/tutorials/marketplace-extensions.md
Normal file
@ -0,0 +1,631 @@
|
||||
# Building Marketplace Extensions in AITBC
|
||||
|
||||
This tutorial shows how to extend the AITBC marketplace with custom features, plugins, and integrations.
|
||||
|
||||
## Overview
|
||||
|
||||
The AITBC marketplace is designed to be extensible. You can add:
|
||||
- Custom auction types
|
||||
- Specialized service categories
|
||||
- Advanced filtering and search
|
||||
- Integration with external systems
|
||||
- Custom pricing models
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 16+
|
||||
- AITBC marketplace source code
|
||||
- Understanding of React/TypeScript
|
||||
- API development experience
|
||||
|
||||
## Step 1: Create a Custom Auction Type
|
||||
|
||||
Let's create a Dutch auction extension:
|
||||
|
||||
```typescript
|
||||
// src/extensions/DutchAuction.ts
|
||||
import { Auction, Bid, MarketplacePlugin } from '../types';
|
||||
|
||||
interface DutchAuctionConfig {
|
||||
startPrice: number;
|
||||
reservePrice: number;
|
||||
decrementRate: number;
|
||||
decrementInterval: number; // in seconds
|
||||
}
|
||||
|
||||
export class DutchAuction implements MarketplacePlugin {
|
||||
config: DutchAuctionConfig;
|
||||
currentPrice: number;
|
||||
lastDecrement: number;
|
||||
|
||||
constructor(config: DutchAuctionConfig) {
|
||||
this.config = config;
|
||||
this.currentPrice = config.startPrice;
|
||||
this.lastDecrement = Date.now();
|
||||
}
|
||||
|
||||
async updatePrice(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const elapsed = (now - this.lastDecrement) / 1000;
|
||||
|
||||
if (elapsed >= this.config.decrementInterval) {
|
||||
const decrements = Math.floor(elapsed / this.config.decrementInterval);
|
||||
this.currentPrice = Math.max(
|
||||
this.config.reservePrice,
|
||||
this.currentPrice - (decrements * this.config.decrementRate)
|
||||
);
|
||||
this.lastDecrement = now;
|
||||
}
|
||||
}
|
||||
|
||||
async validateBid(bid: Bid): Promise<boolean> {
|
||||
await this.updatePrice();
|
||||
return bid.amount >= this.currentPrice;
|
||||
}
|
||||
|
||||
async getCurrentState(): Promise<any> {
|
||||
await this.updatePrice();
|
||||
return {
|
||||
type: 'dutch',
|
||||
currentPrice: this.currentPrice,
|
||||
nextDecrement: this.config.decrementInterval -
|
||||
((Date.now() - this.lastDecrement) / 1000)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Register the Extension
|
||||
|
||||
```typescript
|
||||
// src/extensions/index.ts
|
||||
import { DutchAuction } from './DutchAuction';
|
||||
import { MarketplaceRegistry } from '../core/MarketplaceRegistry';
|
||||
|
||||
const registry = new MarketplaceRegistry();
|
||||
|
||||
// Register Dutch auction
|
||||
registry.registerAuctionType('dutch', DutchAuction, {
|
||||
defaultConfig: {
|
||||
startPrice: 1000,
|
||||
reservePrice: 100,
|
||||
decrementRate: 10,
|
||||
decrementInterval: 60
|
||||
},
|
||||
validation: {
|
||||
startPrice: { type: 'number', min: 0 },
|
||||
reservePrice: { type: 'number', min: 0 },
|
||||
decrementRate: { type: 'number', min: 0 },
|
||||
decrementInterval: { type: 'number', min: 1 }
|
||||
}
|
||||
});
|
||||
|
||||
export default registry;
|
||||
```
|
||||
|
||||
## Step 3: Create UI Components
|
||||
|
||||
```tsx
|
||||
// src/components/DutchAuctionCard.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Progress, Typography } from 'antd';
|
||||
import { useMarketplace } from '../hooks/useMarketplace';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface DutchAuctionCardProps {
|
||||
auction: any;
|
||||
}
|
||||
|
||||
export const DutchAuctionCard: React.FC<DutchAuctionCardProps> = ({ auction }) => {
|
||||
const [currentState, setCurrentState] = useState<any>(null);
|
||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||
const { placeBid } = useMarketplace();
|
||||
|
||||
useEffect(() => {
|
||||
const updateState = async () => {
|
||||
const state = await auction.getCurrentState();
|
||||
setCurrentState(state);
|
||||
setTimeLeft(state.nextDecrement);
|
||||
};
|
||||
|
||||
updateState();
|
||||
const interval = setInterval(updateState, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [auction]);
|
||||
|
||||
const handleBid = async () => {
|
||||
try {
|
||||
await placeBid(auction.id, currentState.currentPrice);
|
||||
} catch (error) {
|
||||
console.error('Bid failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentState) return <div>Loading...</div>;
|
||||
|
||||
const priceProgress =
|
||||
((currentState.currentPrice - auction.config.reservePrice) /
|
||||
(auction.config.startPrice - auction.config.reservePrice)) * 100;
|
||||
|
||||
return (
|
||||
<Card title={auction.title} extra={`Auction #${auction.id}`}>
|
||||
<div className="mb-4">
|
||||
<Title level={4}>Current Price: {currentState.currentPrice} AITBC</Title>
|
||||
<Progress
|
||||
percent={100 - priceProgress}
|
||||
status="active"
|
||||
format={() => `${timeLeft}s until next drop`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<Text type="secondary">
|
||||
Reserve Price: {auction.config.reservePrice} AITBC
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBid}
|
||||
disabled={currentState.currentPrice <= auction.config.reservePrice}
|
||||
>
|
||||
Buy Now
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Step 4: Add Backend API Support
|
||||
|
||||
```python
|
||||
# apps/coordinator-api/src/app/routers/marketplace_extensions.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, List
|
||||
import asyncio
|
||||
|
||||
router = APIRouter(prefix="/marketplace/extensions", tags=["marketplace-extensions"])
|
||||
|
||||
class DutchAuctionRequest(BaseModel):
|
||||
title: str
|
||||
description: str
|
||||
start_price: float
|
||||
reserve_price: float
|
||||
decrement_rate: float
|
||||
decrement_interval: int
|
||||
|
||||
class DutchAuctionState(BaseModel):
|
||||
auction_id: str
|
||||
current_price: float
|
||||
next_decrement: int
|
||||
total_bids: int
|
||||
|
||||
@router.post("/dutch-auction/create")
|
||||
async def create_dutch_auction(request: DutchAuctionRequest) -> Dict[str, str]:
|
||||
"""Create a new Dutch auction."""
|
||||
|
||||
# Validate auction parameters
|
||||
if request.reserve_price >= request.start_price:
|
||||
raise HTTPException(400, "Reserve price must be less than start price")
|
||||
|
||||
# Create auction in database
|
||||
auction_id = await marketplace_service.create_extension_auction(
|
||||
type="dutch",
|
||||
config=request.dict()
|
||||
)
|
||||
|
||||
# Start price decrement task
|
||||
asyncio.create_task(start_price_decrement(auction_id))
|
||||
|
||||
return {"auction_id": auction_id}
|
||||
|
||||
@router.get("/dutch-auction/{auction_id}/state")
|
||||
async def get_dutch_auction_state(auction_id: str) -> DutchAuctionState:
|
||||
"""Get current state of a Dutch auction."""
|
||||
|
||||
auction = await marketplace_service.get_auction(auction_id)
|
||||
if not auction or auction.type != "dutch":
|
||||
raise HTTPException(404, "Dutch auction not found")
|
||||
|
||||
current_price = calculate_current_price(auction)
|
||||
next_decrement = calculate_next_decrement(auction)
|
||||
|
||||
return DutchAuctionState(
|
||||
auction_id=auction_id,
|
||||
current_price=current_price,
|
||||
next_decrement=next_decrement,
|
||||
total_bids=auction.bid_count
|
||||
)
|
||||
|
||||
async def start_price_decrement(auction_id: str):
|
||||
"""Background task to decrement auction price."""
|
||||
while True:
|
||||
await asyncio.sleep(60) # Check every minute
|
||||
|
||||
auction = await marketplace_service.get_auction(auction_id)
|
||||
if not auction or auction.status != "active":
|
||||
break
|
||||
|
||||
new_price = calculate_current_price(auction)
|
||||
await marketplace_service.update_auction_price(auction_id, new_price)
|
||||
|
||||
if new_price <= auction.config["reserve_price"]:
|
||||
await marketplace_service.close_auction(auction_id)
|
||||
break
|
||||
```
|
||||
|
||||
## Step 5: Add Custom Service Category
|
||||
|
||||
```typescript
|
||||
// src/extensions/ServiceCategories.ts
|
||||
export interface ServiceCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
requirements: ServiceRequirement[];
|
||||
pricing: PricingModel;
|
||||
}
|
||||
|
||||
export interface ServiceRequirement {
|
||||
type: 'gpu' | 'cpu' | 'memory' | 'storage';
|
||||
minimum: number;
|
||||
recommended: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface PricingModel {
|
||||
type: 'fixed' | 'hourly' | 'per-unit';
|
||||
basePrice: number;
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export const AI_INFERENCE_CATEGORY: ServiceCategory = {
|
||||
id: 'ai-inference',
|
||||
name: 'AI Inference',
|
||||
icon: 'brain',
|
||||
description: 'Large language model and neural network inference',
|
||||
requirements: [
|
||||
{ type: 'gpu', minimum: 8, recommended: 24, unit: 'GB' },
|
||||
{ type: 'memory', minimum: 16, recommended: 64, unit: 'GB' },
|
||||
{ type: 'cpu', minimum: 4, recommended: 16, unit: 'cores' }
|
||||
],
|
||||
pricing: {
|
||||
type: 'hourly',
|
||||
basePrice: 10,
|
||||
unitPrice: 0.5
|
||||
}
|
||||
};
|
||||
|
||||
// Category registry
|
||||
export const SERVICE_CATEGORIES: Record<string, ServiceCategory> = {
|
||||
'ai-inference': AI_INFERENCE_CATEGORY,
|
||||
'video-rendering': {
|
||||
id: 'video-rendering',
|
||||
name: 'Video Rendering',
|
||||
icon: 'video',
|
||||
description: 'High-quality video rendering and encoding',
|
||||
requirements: [
|
||||
{ type: 'gpu', minimum: 12, recommended: 24, unit: 'GB' },
|
||||
{ type: 'memory', minimum: 32, recommended: 128, unit: 'GB' },
|
||||
{ type: 'storage', minimum: 100, recommended: 1000, unit: 'GB' }
|
||||
],
|
||||
pricing: {
|
||||
type: 'per-unit',
|
||||
basePrice: 5,
|
||||
unitPrice: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Step 6: Create Advanced Search Filters
|
||||
|
||||
```tsx
|
||||
// src/components/AdvancedSearch.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { Select, Slider, Input, Button, Space } from 'antd';
|
||||
import { SERVICE_CATEGORIES } from '../extensions/ServiceCategories';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
|
||||
interface SearchFilters {
|
||||
category?: string;
|
||||
priceRange: [number, number];
|
||||
gpuMemory: [number, number];
|
||||
providerRating: number;
|
||||
}
|
||||
|
||||
export const AdvancedSearch: React.FC<{
|
||||
onSearch: (filters: SearchFilters) => void;
|
||||
}> = ({ onSearch }) => {
|
||||
const [filters, setFilters] = useState<SearchFilters>({
|
||||
priceRange: [0, 1000],
|
||||
gpuMemory: [0, 24],
|
||||
providerRating: 0
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
onSearch(filters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<Search
|
||||
placeholder="Search services..."
|
||||
onSearch={(value) => setFilters({ ...filters, query: value })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="Select category"
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => setFilters({ ...filters, category: value })}
|
||||
allowClear
|
||||
>
|
||||
{Object.values(SERVICE_CATEGORIES).map(category => (
|
||||
<Option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<div>
|
||||
<label>Price Range: {filters.priceRange[0]} - {filters.priceRange[1]} AITBC</label>
|
||||
<Slider
|
||||
range
|
||||
min={0}
|
||||
max={1000}
|
||||
value={filters.priceRange}
|
||||
onChange={(value) => setFilters({ ...filters, priceRange: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>GPU Memory: {filters.gpuMemory[0]} - {filters.gpuMemory[1]} GB</label>
|
||||
<Slider
|
||||
range
|
||||
min={0}
|
||||
max={24}
|
||||
value={filters.gpuMemory}
|
||||
onChange={(value) => setFilters({ ...filters, gpuMemory: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Minimum Provider Rating: {filters.providerRating} stars</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.5}
|
||||
value={filters.providerRating}
|
||||
onChange={(value) => setFilters({ ...filters, providerRating: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="primary" onClick={handleSearch} block>
|
||||
Apply Filters
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Step 7: Add Integration with External Systems
|
||||
|
||||
```python
|
||||
# apps/coordinator-api/src/app/integrations/slack.py
|
||||
import httpx
|
||||
from typing import Dict, Any
|
||||
|
||||
class SlackIntegration:
|
||||
def __init__(self, webhook_url: str):
|
||||
self.webhook_url = webhook_url
|
||||
|
||||
async def notify_new_auction(self, auction: Dict[str, Any]) -> None:
|
||||
"""Send notification about new auction to Slack."""
|
||||
message = {
|
||||
"text": f"New auction created: {auction['title']}",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"*New Auction Alert*\n\n*Title:* {auction['title']}\n"
|
||||
f"*Starting Price:* {auction['start_price']} AITBC\n"
|
||||
f"*Category:* {auction.get('category', 'General')}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "View Auction"},
|
||||
"url": f"https://aitbc.bubuit.net/marketplace/auction/{auction['id']}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(self.webhook_url, json=message)
|
||||
|
||||
async def notify_bid_placed(self, auction_id: str, bid_amount: float) -> None:
|
||||
"""Notify when a bid is placed."""
|
||||
message = {
|
||||
"text": f"New bid of {bid_amount} AITBC placed on auction {auction_id}"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(self.webhook_url, json=message)
|
||||
|
||||
# Integration with Discord
|
||||
class DiscordIntegration:
|
||||
def __init__(self, webhook_url: str):
|
||||
self.webhook_url = webhook_url
|
||||
|
||||
async def send_embed(self, title: str, description: str, fields: list) -> None:
|
||||
"""Send rich embed message to Discord."""
|
||||
embed = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"fields": fields,
|
||||
"color": 0x00ff00
|
||||
}
|
||||
|
||||
payload = {"embeds": [embed]}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(self.webhook_url, json=payload)
|
||||
```
|
||||
|
||||
## Step 8: Create Custom Pricing Model
|
||||
|
||||
```typescript
|
||||
// src/extensions/DynamicPricing.ts
|
||||
export interface PricingRule {
|
||||
condition: (context: PricingContext) => boolean;
|
||||
calculate: (basePrice: number, context: PricingContext) => number;
|
||||
}
|
||||
|
||||
export interface PricingContext {
|
||||
demand: number;
|
||||
supply: number;
|
||||
timeOfDay: number;
|
||||
dayOfWeek: number;
|
||||
providerRating: number;
|
||||
serviceCategory: string;
|
||||
}
|
||||
|
||||
export class DynamicPricingEngine {
|
||||
private rules: PricingRule[] = [];
|
||||
|
||||
addRule(rule: PricingRule) {
|
||||
this.rules.push(rule);
|
||||
}
|
||||
|
||||
calculatePrice(basePrice: number, context: PricingContext): number {
|
||||
let finalPrice = basePrice;
|
||||
|
||||
for (const rule of this.rules) {
|
||||
if (rule.condition(context)) {
|
||||
finalPrice = rule.calculate(finalPrice, context);
|
||||
}
|
||||
}
|
||||
|
||||
return Math.round(finalPrice * 100) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Example pricing rules
|
||||
export const DEMAND_SURGE_RULE: PricingRule = {
|
||||
condition: (ctx) => ctx.demand / ctx.supply > 2,
|
||||
calculate: (price) => price * 1.5, // 50% surge
|
||||
};
|
||||
|
||||
export const PEAK_HOURS_RULE: PricingRule = {
|
||||
condition: (ctx) => ctx.timeOfDay >= 9 && ctx.timeOfDay <= 17,
|
||||
calculate: (price) => price * 1.2, // 20% peak hour premium
|
||||
};
|
||||
|
||||
export const TOP_PROVIDER_RULE: PricingRule = {
|
||||
condition: (ctx) => ctx.providerRating >= 4.5,
|
||||
calculate: (price) => price * 1.1, // 10% premium for top providers
|
||||
};
|
||||
|
||||
// Usage
|
||||
const pricingEngine = new DynamicPricingEngine();
|
||||
pricingEngine.addRule(DEMAND_SURGE_RULE);
|
||||
pricingEngine.addRule(PEAK_HOURS_RULE);
|
||||
pricingEngine.addRule(TOP_PROVIDER_RULE);
|
||||
|
||||
const finalPrice = pricingEngine.calculatePrice(100, {
|
||||
demand: 100,
|
||||
supply: 30,
|
||||
timeOfDay: 14,
|
||||
dayOfWeek: 2,
|
||||
providerRating: 4.8,
|
||||
serviceCategory: 'ai-inference'
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Your Extensions
|
||||
|
||||
```typescript
|
||||
// src/extensions/__tests__/DutchAuction.test.ts
|
||||
import { DutchAuction } from '../DutchAuction';
|
||||
|
||||
describe('DutchAuction', () => {
|
||||
let auction: DutchAuction;
|
||||
|
||||
beforeEach(() => {
|
||||
auction = new DutchAuction({
|
||||
startPrice: 1000,
|
||||
reservePrice: 100,
|
||||
decrementRate: 10,
|
||||
decrementInterval: 60
|
||||
});
|
||||
});
|
||||
|
||||
test('should start with initial price', () => {
|
||||
expect(auction.currentPrice).toBe(1000);
|
||||
});
|
||||
|
||||
test('should decrement price after interval', async () => {
|
||||
// Mock time passing
|
||||
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 60000);
|
||||
|
||||
await auction.updatePrice();
|
||||
expect(auction.currentPrice).toBe(990);
|
||||
});
|
||||
|
||||
test('should not go below reserve price', async () => {
|
||||
// Mock significant time passing
|
||||
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 600000);
|
||||
|
||||
await auction.updatePrice();
|
||||
expect(auction.currentPrice).toBe(100);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
1. **Build your extensions**:
|
||||
```bash
|
||||
npm run build:extensions
|
||||
```
|
||||
|
||||
2. **Deploy to production**:
|
||||
```bash
|
||||
# Copy extension files
|
||||
cp -r src/extensions/* /var/www/aitbc.bubuit.net/marketplace/extensions/
|
||||
|
||||
# Update API
|
||||
scp apps/coordinator-api/src/app/routers/marketplace_extensions.py \
|
||||
aitbc:/opt/coordinator-api/src/app/routers/
|
||||
|
||||
# Restart services
|
||||
ssh aitbc "sudo systemctl restart coordinator-api"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Modular Design** - Keep extensions independent
|
||||
2. **Backward Compatibility** - Ensure extensions work with existing marketplace
|
||||
3. **Performance** - Optimize for high-frequency operations
|
||||
4. **Security** - Validate all inputs and permissions
|
||||
5. **Documentation** - Document extension APIs and usage
|
||||
|
||||
## Conclusion
|
||||
|
||||
This tutorial covered creating marketplace extensions including custom auction types, service categories, advanced search, and external integrations. You can now build powerful extensions to enhance the AITBC marketplace functionality.
|
||||
|
||||
For more examples and community contributions, visit the marketplace extensions repository.
|
||||
Reference in New Issue
Block a user