Copy # app.py
from flask import Flask, request, jsonify
from functools import wraps
import hmac
import hashlib
import time
from config import Config
from tasks import (
create_order_task,
cancel_order_task,
check_price_and_order,
monitor_orders
)
from clore_client import CloreClient
app = Flask(__name__)
# Rate limiting storage
request_counts = {}
def verify_signature(f):
"""Verify webhook signature."""
@wraps(f)
def decorated(*args, **kwargs):
signature = request.headers.get('X-Webhook-Signature')
if not signature:
return jsonify({"error": "Missing signature"}), 401
# Calculate expected signature
payload = request.get_data()
expected = hmac.new(
Config.WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({"error": "Invalid signature"}), 401
return f(*args, **kwargs)
return decorated
def rate_limit(f):
"""Simple rate limiting."""
@wraps(f)
def decorated(*args, **kwargs):
ip = request.remote_addr
now = time.time()
# Clean old entries
request_counts[ip] = [t for t in request_counts.get(ip, []) if now - t < 60]
if len(request_counts.get(ip, [])) >= Config.MAX_REQUESTS_PER_MINUTE:
return jsonify({"error": "Rate limit exceeded"}), 429
request_counts.setdefault(ip, []).append(now)
return f(*args, **kwargs)
return decorated
# === Webhook Endpoints ===
@app.route('/webhook/create-order', methods=['POST'])
@rate_limit
@verify_signature
def webhook_create_order():
"""
Create a new GPU rental order.
Request body:
{
"gpu_type": "RTX 4090",
"max_price": 0.50,
"image": "nvidia/cuda:12.8.0-base-ubuntu22.04",
"use_spot": true,
"callback_url": "https://your-server.com/callback"
}
"""
data = request.json
gpu_type = data.get('gpu_type')
if not gpu_type:
return jsonify({"error": "gpu_type required"}), 400
# Queue the task
task = create_order_task.delay(
gpu_type=gpu_type,
max_price=data.get('max_price'),
image=data.get('image'),
use_spot=data.get('use_spot', True),
callback_url=data.get('callback_url')
)
return jsonify({
"status": "queued",
"task_id": task.id,
"message": f"Order creation queued for {gpu_type}"
})
@app.route('/webhook/cancel-order', methods=['POST'])
@rate_limit
@verify_signature
def webhook_cancel_order():
"""
Cancel an existing order.
Request body:
{
"order_id": 12345,
"callback_url": "https://your-server.com/callback"
}
"""
data = request.json
order_id = data.get('order_id')
if not order_id:
return jsonify({"error": "order_id required"}), 400
task = cancel_order_task.delay(
order_id=order_id,
callback_url=data.get('callback_url')
)
return jsonify({
"status": "queued",
"task_id": task.id,
"message": f"Cancellation queued for order {order_id}"
})
@app.route('/webhook/price-alert', methods=['POST'])
@rate_limit
@verify_signature
def webhook_price_alert():
"""
Set up price-based auto-ordering.
Request body:
{
"gpu_type": "RTX 4090",
"target_price": 0.35,
"image": "pytorch/pytorch:2.7.1-cuda12.8-cudnn9-runtime",
"callback_url": "https://your-server.com/callback"
}
"""
data = request.json
gpu_type = data.get('gpu_type')
target_price = data.get('target_price')
if not gpu_type or not target_price:
return jsonify({"error": "gpu_type and target_price required"}), 400
task = check_price_and_order.delay(
gpu_type=gpu_type,
target_price=target_price,
image=data.get('image'),
callback_url=data.get('callback_url')
)
return jsonify({
"status": "queued",
"task_id": task.id,
"message": f"Price alert set for {gpu_type} at ${target_price}/hr"
})
@app.route('/webhook/status', methods=['POST'])
@rate_limit
@verify_signature
def webhook_status():
"""
Get status of all orders.
Request body:
{
"callback_url": "https://your-server.com/callback"
}
"""
data = request.json or {}
task = monitor_orders.delay(
callback_url=data.get('callback_url')
)
return jsonify({
"status": "queued",
"task_id": task.id
})
@app.route('/webhook/marketplace', methods=['GET'])
@rate_limit
def webhook_marketplace():
"""Get current marketplace status (no auth required for read-only)."""
try:
client = CloreClient()
servers = client.get_marketplace()
# Summarize by GPU type
summary = {}
for s in servers:
if not s.gpus:
continue
gpu = s.gpus[0].split()[0] if s.gpus else "Unknown"
if gpu not in summary:
summary[gpu] = {"available": 0, "total": 0, "min_price": float('inf')}
summary[gpu]["total"] += 1
if s.is_available:
summary[gpu]["available"] += 1
if s.spot_price:
summary[gpu]["min_price"] = min(summary[gpu]["min_price"], s.spot_price)
# Clean up infinity
for gpu in summary:
if summary[gpu]["min_price"] == float('inf'):
summary[gpu]["min_price"] = None
return jsonify({
"status": "ok",
"summary": summary,
"total_servers": len(servers),
"total_available": len([s for s in servers if s.is_available])
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# === Slack/Discord Slash Commands ===
@app.route('/slack/command', methods=['POST'])
def slack_command():
"""Handle Slack slash commands."""
# Verify Slack request (in production, verify signing secret)
text = request.form.get('text', '').strip()
parts = text.split()
if not parts:
return jsonify({
"response_type": "ephemeral",
"text": "Usage: /clore [rent|cancel|status] [args]"
})
command = parts[0].lower()
if command == 'rent':
# /clore rent RTX 4090 0.50
if len(parts) < 2:
return jsonify({
"response_type": "ephemeral",
"text": "Usage: /clore rent <gpu_type> [max_price]"
})
gpu_type = parts[1]
max_price = float(parts[2]) if len(parts) > 2 else None
create_order_task.delay(gpu_type=gpu_type, max_price=max_price)
return jsonify({
"response_type": "in_channel",
"text": f"π Creating order for {gpu_type}..."
})
elif command == 'cancel':
# /clore cancel 12345
if len(parts) < 2:
return jsonify({
"response_type": "ephemeral",
"text": "Usage: /clore cancel <order_id>"
})
order_id = int(parts[1])
cancel_order_task.delay(order_id=order_id)
return jsonify({
"response_type": "in_channel",
"text": f"π Cancelling order {order_id}..."
})
elif command == 'status':
monitor_orders.delay()
return jsonify({
"response_type": "in_channel",
"text": "π Fetching order status..."
})
else:
return jsonify({
"response_type": "ephemeral",
"text": f"Unknown command: {command}"
})
# === Health Check ===
@app.route('/health', methods=['GET'])
def health():
return jsonify({"status": "ok"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=Config.PORT, debug=False)